2023. 7. 12. 15:58ㆍDAML
이번 섹션에서는 Canton architecture에 대해서 감을 잡고, high-level flows, entities, components의 모양을 그려보는 과정을 진행합니다. 그리고 약간의 가정(아직은 뭔 가정인지는 모르겠지만)을 한다고 하네요.
1. Canton 이 뭐야?
- Daml ledger interoperatbility(상호 운용성) protocol
- 상호 운용성이란 다른 여러 시스템과 호환되어 사용이 가능하도록 함을 의미하는데, 구체적 의미는 아직 모르겠네요.
- 서로 다른 Daml ledger들을 하나의 virtual global ledger로 연결해주는 역할을 해요.
- 즉, virtual global ledger을 만들어요.
- 새로운 party, ledger, application위에 만들어지는 application 등을 문제없이 추가(확장)할 수 있도록 해요.
- 위와 같은 확장은 중앙관리자가 필요없고, global network에 대한 합의 절차 또한 필요없어요.
- Scala로 작성되었고, 데이터베이스는 Java 프로세스(JVM에서)로 실행돼요.
- 즉, native로 실행하려면 Java 11 이상이 설치되어 있어야 하고, docker와 같은 클라우드에서도
2. Daml이랑 Canton이랑 뭐가 다른거야?
- Daml은 스마트 계약 언어로, 특정 계약을 조회 혹은 변경할 수 있는 권한을 명시해주는 역할을 해요.
- Canton synchronization protocol은 위에서 정한 권한들을 강제하고, 수상한 party가 있더라도 데이터가 굉장히 안전하게 공유되고 있다는 것을 보장하도록 작동해요.
3. 간단한 예시를 통해 이해해보자!
Canton protocol을 작동시키려면 participant node 들은 하나 이상의 synchronization domain에 연결되어 있어야 해요. 그리고 transaction을 실행시키려면 participant node 안의 모든 party들이 하나의 domain에 연결되어 있어야 해요 여기서 transaction은 여러 party들이 공유하는 contract가 update되는 변화를 의미해요.
총 3개의 Canton API가 눈에 보이죠? 얘네들을 이해해야해요.
<Ledger API>
각 participant node들은 본인한테 속한 party들이 node에 존재하는 ledger에 Ledger API를 통해서 접근하게 해줘요. Ledger API를 console 창에서 입력해서 사용해도 되지만, 어플리케이션 사용자의 컴퓨터 화면에 뜨는 알아보기 쉬운 인터페이스로 Ledger를 핸들링하는 것이 좋겠죠.
<Admin API>
각 participant node는 Admin API르 제공해요. administrator("너"라고 하네요 공식문서에서)가 여러가지 행동을 하게 해주는 인터페이스예요. 총 5가지 능력이 있다고 해요.
- participant node들과 domain과의 연결을 관리해요. (enable, disable하는 형태로 작동)
- participant node에서 호스트되는 party들을 추가, 삭제해요.
- 새로운 Daml archives를 Ledger에 업로드해요.
- participant node 운영에 필요한 정보들을 configure해요. (중요한 key들을 관리)
- diagnostic command을 실행해요. (ICMP와 같은 ping과정 -> 모르겠으면 네트워크 과목을 듣고오세요!)
<Public API>
각 domain node는 public API를 제공해요. 이는 participant node와 synchronization domain과의 통신을 위한 인터페이스예요.
이러한 배경지식을 가지고 위의 Canton Network를 구성해보도록 해요.
[Configuration file로 Canton Network를 초기화하기]
bin/canton -c examples/01-simple-topology/simple-topology.conf
공식문서를 통해 깃허브를 들어가서 Canton을 받아오고, 위의 명령어를 입력해요. 이게 돌아가려면 Canton이 있어야 하고, simple-topology.conf라는 configuration file이 해당 경로에 있어야해요.
저 configuration file의 내용은 아래와 같아요!
canton {
participants {
participant1 {
storage.type = memory
admin-api.port = 5012
ledger-api.port = 5011
}
participant2 {
storage.type = memory
admin-api.port = 5022
ledger-api.port = 5021
}
}
domains {
mydomain {
storage.type = memory
public-api.port = 5018
admin-api.port = 5019
}
}
// enable ledger_api commands for our getting started guide
features.enable-testing-commands = yes
}
HOCON 형식으로 되어 있어서 보기가 편하죠? 현재 Canton networ의 participant에는 participant1, participant2가 있고, domain으로는 mydomain이 있어요. 각각은 storage.type에 storage backend를 명시해주는데, 현재는 in-memory storage를 이용하려고 하니까 memory라고 써주있어요. 그리고 각 participant에서는 Ledger API와 Admin API를 지원해야 하므로 이를 위한 포트번호를 부여해주었고, domain의 경우에는 Public API와 Admin API를 지원해야 하므로 이를 위한 포트번호를 부여해주었어요!
[Participant들을 Domain에 연결하기]
위에서는 어떤 participant들과 어떤 domain이 있는지를 configuration file가 명시해주었어요. 그런데 서로의 연결관계는 전혀 모르는 상황이에요. 얘네들을 연결해주어야 분산 원장이 구현이 가능하기 때문에 그 과정을 진행할 거예요.
현재 상태를 확인해보면 아래와 같아요.
@ health.status
res1: EnterpriseCantonStatus = Status for Domain 'mydomain':
Domain id: mydomain::12209a32701fdd57f6cb3478082a11343d7a11b880df91b2e33bcd630ea8b02ffc7d
Uptime: 4.527076s
Ports:
admin: 30031
public: 30030
Connected Participants: None -------------------------------> 연결되어 있는 participant가 없다!
Sequencer: SequencerHealthStatus(isActive = true)
Status for Participant 'participant1':
Participant id: PAR::participant1::122094bb12b5e68fcb198076102c04ccf77947c6386bc8682c61dce0a696621a4961
Uptime: 3.359776s
Ports:
ledger: 30026
admin: 30027
Connected domains: None ------------------------------------> 연결되어 있는 domain이 없다!
Unhealthy domains: None
Active: true
Status for Participant 'participant2':
Participant id: PAR::participant2::12200383d5e2c5ae1fa66d4a74541a1e6a18a233c363abd7fbcb85ee025bfd1fe2e5
Uptime: 2.269386s
Ports:
ledger: 30028
admin: 30029
Connected domains: None ------------------------------------> 연결되어 있는 domain이 없다!
Unhealthy domains: None
Active: true
그런데 아래의 과정을 거치면 participant들과 domain이 연결이 돼요!
participant1.domains.connect_local(mydomain)
participant2.domains.connect_local(mydomain)
위의 두 줄을 통해서 participant1이 mydomain에 연결이 되었고, participant2가 mydomain에 연결이 되었어요.
상태를 다시 확인해보면 아래와 같아요.
@ health.status
res2: EnterpriseCantonStatus = Status for Domain 'mydomain':
Domain id: mydomain::12209a32701fdd57f6cb3478082a11343d7a11b880df91b2e33bcd630ea8b02ffc7d
Uptime: 7.330346s
Ports:
admin: 30031
public: 30030
Connected Participants:
PAR::participant2::12200383d5e2... --------------------->participant2가 연결되었어요.
PAR::participant1::122094bb12b5... --------------------->participant1이 연결되었어요.
Sequencer: SequencerHealthStatus(isActive = true)
Status for Participant 'participant1':
Participant id: PAR::participant1::122094bb12b5e68fcb198076102c04ccf77947c6386bc8682c61dce0a696621a4961
Uptime: 6.144685s
Ports:
...
연결이 잘 되었다는 것을 확인할 수 있죠??
이 연결의 성능을 테스트 할 수도 있는데 네트워크에서 배운 ICMP ping에서 영감을 받았다는 command가 있어요.
@ participant1.health.ping(participant2)
res3: Duration = 381 milliseconds
두 participant 사이의 Ledger API들의 roundtrip time을 반환해주는 과정이에요.
공식문서에서는 신기하게도 첫번째 시도에서는 JVM이 워밍업이 아직 안되어서 느리고, 두번째 시도에는 급격히 시간이 적어지며, JVM의 just-in-time compilation 지나면 또 줄어든다고 하네요. --> 여긴 사실 저도 잘 이해가 안되는 부분입니다..
[Party 만들기]
각 participant node들은 여러 party들을 호스팅할 수 있고, 해당 party의 identity는 해당 participant node의 namespace가 돼요. 갑자기 namespace가 튀어나왔으니 아래를 통해서 알려드릴게요~
@ participant1.id
res15: ParticipantId = PAR::participant1::12204c005072...
Canton에서는 party, participant, domain이 모두 unique identifier로 표현이 돼요. 이 때, unique identifier는 두 부분으로 나뉘는데, 사람이 읽을 수 있는 문자열 그리고 16진수 public key가 그것이예요. 둘은 보시다시미 :: 으로 구분이 되고 있어요. 여기서 public key 부분이 바로 namespace인 것이예요!
이 때 participant1에 alice라는 party를 추가해주기 위해서는 아래의 명령어를 콘솔에 입력하면 돼요.
@ var alice = participant1.parties.enable("Alice")
alice: PartyId = Alice::12204c005072...
namespace 관점에서 보면 participant1의 namespace와 Alice라는 party의 namespace가 동일하다는 것을 확인할 수 있어요. 이 둘은 사람이 읽을 수 있는 문자열을 통해 구분이 되는 것으로 보여요.
participant2에는 bob을 party로 추가해줄게요.
@ var bob = participant1.parties.enable("Bob")
bob: PartyId = Bob::1220e1bf5351...
[Smart Contract Code를 각 participant node들에게 제공해주기 -> DAR file 업로드하기]
Alice와 Bob 사이의 smart contract가 생성되려면, 각 participant node에서 해당 smart contract의 template를 알고 있어야 해요. Canton은 Daml Code로 적힌 smart contract를 지원해요. 따라서, Daml templates가 들어있는 DAR file을 participant1, participant2에 모두 제공해주어야 하고, 이는 아래 코드로 진행해요.
@ participants.all.dars.upload("dars/CantonExamples.dar")
res27: Map[com.digitalasset.canton.console.ParticipantReference, String] = Map(
Participant 'participant1' -> "1220cf8e19cb32ce97bb7f745716c46068753d391b7100fa13511be4954015c81057",
Participant 'participant2' -> "1220cf8e19cb32ce97bb7f745716c46068753d391b7100fa13511be4954015c81057"
)
CantonExmaples.dar 파일은 아래의 daml code로 부터 컴파일이 되었다고 공식문서에서 이야기해요.
Iou는 누가 돈을 만들어서 누구에게 주는 지에 대한 내용이 적힌 template이고,
PaintHouse, OfferToPaintHouseByPainter template은 어떤 painter가 누구 집을 paint해주고 돈을 얼마 받는 지가 적히는 template이에요.
template Iou
with
payer: Party
owner: Party
amount: Amount
viewers: [Party]
where
ensure (amount.value >= 0.0)
signatory payer
observer owner
observer viewers
choice Call : ContractId GetCash
controller owner
do
create GetCash with payer; owner; amount
choice Transfer : ContractId Iou
with
newOwner: Party
controller owner
do
create this with owner = newOwner; viewers = []
choice Share : ContractId Iou
with
viewer : Party
controller owner
do
create this with viewers = (viewer :: viewers)
module Paint where
import Daml.Script
import Iou
template PaintHouse
with
painter: Party
houseOwner: Party
where
signatory painter, houseOwner
agreement
show painter <> " will paint the house of " <> show houseOwner
template OfferToPaintHouseByPainter
with
houseOwner: Party
painter: Party
bank: Party
amount: Amount
where
signatory painter
observer houseOwner
choice AcceptByOwner : ContractId Iou
with
iouId : ContractId Iou
controller houseOwner
do
iouId2 <- exercise iouId Transfer with newOwner = painter
paint <- create $ PaintHouse with painter; houseOwner
return iouId2
잘 올라갔는 지 확인하려면 아래처럼 입력하면 돼요!
@ participant1.dars.list()
res25: Seq[com.digitalasset.canton.participant.admin.v0.DarDescription] = Vector(
DarDescription(hash = "1220aa9cedfab48270477d1ec174272836da3a6e8b5963ea8c83bd55556421ad2412", name = "AdminWorkflowsWithVacuuming"),
DarDescription(hash = "1220cf8e19cb32ce97bb7f745716c46068753d391b7100fa13511be4954015c81057", name = "CantonExamples")
)
현재 participant1에 올라가있는 DAR file은 AdminWorkflowsWithVacuuming과 CantonExamples 로 총 2개의 DAR file로 올라가있어요. AdminWorkflowsWithVacuuming 파일은 Canton이 켜진 직후에도 올라가있는, 즉 아무것도 안해도 미리 올라가있는 DAR file인 것이고, 위의 코드에서 CantonExamples.dar를 추가적으로 올려주었기 때문에 해당 파일도 participant1에 업로드 되어 있다고 이야기를 해주는 거예요.
그런데 잘 보시면, domain에는 dar 파일을 올리지 않아요. 왜냐하면, domain은 Daml code나 smart contract에 대한 내용을 전혀 알 필요 없기 때문이죠. 모든 synchronization 과정은 participant node들 사이의 통신으로 진행이 돼요.
[Smart contract를 실행하기]
공식문서에서는 Alice네 집에 Bob이 청소를 해주는 대가로 돈을 주는 상황을 예시 앱으로 만들어서 Ledger API를 어떻게 사용하는 지 알려주고 있어요. 그럴려면 Alice, Bob 그리고 돈을 발행하는 Bank가 있어야겠죠? 그런데 우리가 만든 Canton Network에는 bank는 없으니까 bank party도 만들어줘요. participant1, participant2 둘 중 어디에 속해도 상관없어서 공식문서에서는 participant2에 속하게 해주었어요.
val bank = participant2.parties.enable("Bank", waitForDomain = DomainChoice.All)
bank: PartyId = Bank::1220e1bf5351...
여기서 waitForDomain 부분이 위와 조금 다른데, 아직 사용되기 이전에 분산 시스템이 알고 있으라고 적어놓는 거라고 하는데 아직은 잘 안와닿네요 저는... 일단 새로운 party가 participant2에 추가되었다고 이해해도 예시앱을 진행하는 데는 큰 문제가 없어요!
이제 Iou를 만들어야 해요. 우리는 해당 template을 가지고 있는 binary module을 sha256으로 해싱한 package id가 필요해요.
val pkgIou = participant1.packages.find("Iou").head
pkgIou: com.digitalasset.canton.participant.admin.v0.PackageDescription = PackageDescription(packageId = "eabed7ee422fc250c9299d6bf2dddafc9104d8948dd5d1ac1c506649c29eb975", sourceDescription = "CantonExamples")
이렇게 얻어진 package id를 이용하면 IOU를 만들 수 있어요.
payer : bank
owner : alice
amount.value = 100.0
amount.currency = "EUR"
viewers = []
가 들어있는 contract를 생성하는 명령어를 아래 적어놓았어요.
@ val createIouCmd = ledger_api_utils.create(pkgIou.packageId,"Iou","Iou",Map("payer" -> bank,"owner" -> alice,"amount" -> Map("value" -> 100.0, "currency" -> "EUR"),"viewers" -> List()))
createIouCmd : com.daml.ledger.api.v1.commands.Command = Command(
command = Create(
value = CreateCommand(
templateId = Some(
value = Identifier(
packageId = "eabed7ee422fc250c9299d6bf2dddafc9104d8948dd5d1ac1c506649c29eb975",
..
생성이 끝났다면 Ledger API를 이용해서 실제 ledger에 submit을 해봐야겠죠? 아래 그 코드와 그 결과가 적혀있어요.
bank라는 signatory가 createIouCmd를 ledger에 submit하는 과정을 나타낸 것이에요.
@ participant2.ledger_api.commands.submit(Seq(bank), Seq(createIouCmd))
res35: com.daml.ledger.api.v1.transaction.TransactionTree = TransactionTree(
transactionId = "1220c006803211ca5f353025057c626d0ac09f23cf349a18adf6d6d675dc3d03cbf2",
commandId = "7843d9a4-9680-45c9-af1c-55dfce2c0fa7",
workflowId = "",
effectiveAt = Some(
value = Timestamp(
seconds = 1687178983L,
nanos = 464195000,
unknownFields = UnknownFieldSet(fields = Map())
)
),
offset = "000000000000000015",
..
daml authorization logic에 의해서 signatory만이 transaction을 submit을 할 수 있으므로 아래와 같은 코드는 문제가 있어요.
@ participant2.ledger_api.commands.submit(Seq(alice), Seq(createIouCmd)) -> alice는 signatory가 아님!
@ participant1.ledger_api.commands.submit(Seq(bank), Seq(createIouCmd)) -> bank는 participant2에 있음!
그러나 alice는 observer는 맞기 때문에 아래 코드로 이 transaction을 볼 수 있어요.
templateID가 Iou.Iou인 contract을 확인하기 위한 코드예요.
ACS는 active contract set을 의미해요. 말그대로 현재 active한 contract들의 모임이죠.
@ val aliceIou = participant1.ledger_api.acs.find_generic(alice, _.templateId == "Iou.Iou")
aliceIou : com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent = WrappedCreatedEvent(
event = CreatedEvent(
eventId = "#1220c006803211ca5f353025057c626d0ac09f23cf349a18adf6d6d675dc3d03cbf2:0",
contractId = "00a21a1ac67786c8e5b938a072c9b70193138ca74dca98bc7d91a491de93517b4bca0112205c7a719b3a81453f02a616d9e78f7f10902a68f2e9fc9cf63a423cfb2dd62d9b",
..
아래 코드로도 alice의 ACS를 확인할 수 있어요.
@ participant1.ledger_api.acs.of_party(alice)
res37: Seq[com.digitalasset.canton.admin.api.client.commands.LedgerApiTypeWrappers.WrappedCreatedEvent] = List(
WrappedCreatedEvent(
event = CreatedEvent(
eventId = "#1220c006803211ca5f353025057c626d0ac09f23cf349a18adf6d6d675dc3d03cbf2:0",
contractId = "00a21a1ac67786c8e5b938a072c9b70193138ca74dca98bc7d91a491de93517b4bca0112205c7a719b3a81453f02a616d9e78f7f10902a68f2e9fc9cf63a423cfb2dd62d9b",
templateId = Some(
value = Identifier(
packageId = "eabed7ee422fc250c9299d6bf2dddafc9104d8948dd5d1ac1c506649c29eb975",
..
근데 이거는 보시다시피 반복적이고 당장은 필요없는 정보들도 모두 출력해줘요. 아래처럼 작성하면 contract의 필드와 그 값만을 받아보는 것이 가능해요.
@ participant1.ledger_api.acs.of_party(alice).map(x => (x.templateId, x.arguments))
res38: Seq[(String, Map[String, Any])] = List(
(
"Iou.Iou",
HashMap(
"payer" -> "Bank::12200383d5e2c5ae1fa66d4a74541a1e6a18a233c363abd7fbcb85ee025bfd1fe2e5",
"viewers" -> List(elements = Vector()),
"owner" -> "Alice::122094bb12b5e68fcb198076102c04ccf77947c6386bc8682c61dce0a696621a4961",
"amount.currency" -> "EUR",
"amount.value" -> "100.0000000000"
)
)
)
깔끔해졌죠??
현재 alice는 bank가 100유로를 준 상황이에요. 이제 bob이 alice의 집을 페인트칠해주면서 돈을 받는 부분을 구현해볼게요. paint 관련 template을 받아오기 위해 package id를 받아올게요.
@ val pkgPaint = participant1.packages.find("Paint").head
pkgPaint : com.digitalasset.canton.participant.admin.v0.PackageDescription = PackageDescription(
packageId = "eabed7ee422fc250c9299d6bf2dddafc9104d8948dd5d1ac1c506649c29eb975",
sourceDescription = "CantonExamples"
)
이렇게 한다음 bob이 alice에게 서비스를 제공해준다는 contract를 Ledger API를 사용해서 만들어줘요.
@ val createOfferCmd = ledger_api_utils.create(pkgPaint.packageId, "Paint", "OfferToPaintHouseByPainter", Map("bank" -> bank, "houseOwner" -> alice, "painter" -> bob, "amount" -> Map("value" -> 100.0, "currency" -> "EUR")))
createOfferCmd : com.daml.ledger.api.v1.commands.Command = Command(
command = Create(
value = CreateCommand(
templateId = Some(
value = Identifier(
packageId = "eabed7ee422fc250c9299d6bf2dddafc9104d8948dd5d1ac1c506649c29eb975",
..
만들었다면 실제 ledger에 submit을 해주어야겠죠? 물론 이 template에서의 signatory는 bob이므로 bob이 제출해주어야 해요.
@ participant2.ledger_api.commands.submit_flat(Seq(bob), Seq(createOfferCmd))
res41: com.daml.ledger.api.v1.transaction.Transaction = Transaction(
transactionId = "1220eef8e9a1640b08314ffe9866ff89a7edbd52fcf729f8bf0da5869e1e5217a63f",
commandId = "b6f408c5-5610-46fa-8c56-6fe3a42936aa",
workflowId = "",
effectiveAt = Some(
value = Timestamp(
..
이 contract는 당연히 alice도 볼 수 있겠죠. 하지만, bank는 observer가 아니므로 내용을 볼 수는 없어요. 누가 어떤 contract를 볼 수 있는 지를 간단히 정리해볼게요.
현재 만들어진 contract는 총 2개예요.
1. alice와 bank 사이의 IOU contract
2. alice와 bob 사이의 paintOffer contract
alice는 1, 2번 모두 볼 수 있고, bank는 1번만 볼 수 있고, bob은 2번만 볼 수있어요. 만약 제 3의 participant node가 있다면 그 노드의 party는 1, 2번을 모두 볼 수 없을 것이에요. 이는 Canton protocol의 특이한 점으로 sub-transaction privacy라고 불러요. 즉, Canton protocol은 자격이 있는 participant들만이 정보를 받아볼 수 있고, domain은 정보가 거쳐가긴하지만, 암호화되어서 받아볼 수 없는 형태예요.
bob이 alice 집의 페인트칠을 끝냈다면 돈을 줘야겠죠? 이는 OfferToPaintHouseByPainter template의 AcceptByOwner choice로 구현이 되어 있고, 해당 choice에서는 alice가 bob에게 Iou를 transfer 하고, paint가 되었다는 것을 기록해요. 이러한 logic은 아래의 코드로 구현이 돼요.
먼저, Iou contract Id를 맞는 형식으로 전달해주기 위해 import해요.
@ import com.digitalasset.canton.protocol.LfContractId
다음으로는 aliceIou의 일부분을 파라미터로 넣어주고 choice를 실행시켜줘요. alice가 bob의 제안을 받아들인 상황을 만들었어요.
@ val acceptOffer = ledger_api_utils.exercise("AcceptByOwner", Map("iouId" -> LfContractId.assertFromString(aliceIou.event.contractId)),paintOffer.event)
acceptOffer : com.daml.ledger.api.v1.commands.Command = Command(
command = Exercise(
value = ExerciseCommand(
templateId = Some(
value = Identifier(
packageId = "eabed7ee422fc250c9299d6bf2dddafc9104d8948dd5d1ac1c506649c29eb975",
..
제안을 받아들였다는 것을 alice가 ledger에 submit 해주는 과정이예요.
@ participant1.ledger_api.commands.submit_flat(Seq(alice), Seq(acceptOffer))
res48: com.daml.ledger.api.v1.transaction.Transaction = Transaction(
transactionId = "122041db0e3caf02b48df28d88d88ca16b347902c502f46fa1edabed4f4fa3e8a94c",
commandId = "327ce974-2b67-4e63-b246-cbe13d102421",
workflowId = "",
effectiveAt = Some(
value = Timestamp(
..
이렇게 하면 bank는 모르게 alice에서 bob으로 Iou가 넘어간 것을 확인할 수 있어요.
우리는 간단한 과정을 진행하려고 Ledger API를 직접 실행시켜보았어요. 그런데 그렇게 하지 않고 할 수 있는 방법은 여러가지예요.
1. Navigator (browser version)
2. Navigator (console version)
3. Daml script
4. Daml trigger
5. Daml REPL
6. JSON API
7. 다른 언어로 binding하기 (이건 뭐지..?)
4. bootstrap script으로 반복적인 작업 자동화하기
bin/canton -c examples/01-simple-topology/simple-topology.conf
위의 명령어는 simple-topology.conf에서 정의해놓은 것처럼 두 개의 participant node 그리고 하나의 domain, 그리고 각각에서 API를 사용하기 위한 port번호들이 적혀있어요. 그런데, participant와 domain간의 연결관계는 그 이후 추가적인 작업을 진행해주었어요. 근데 이 과정은 반복적인 작업이므로 bootstrap script으로 만들어놓는 것이 편리해요!
bin/canton -c examples/01-simple-topology/simple-topology.conf --bootstrap examples/01-simple-topology/simple-topology.canton
위 명령어를 사용하고, simple-topology.canton이라는 파일에 script를 작성해놓으면 돼요. 여기에는 Canton Console에서 실행가능한 모든 명렁어를 입력해놓는 것이 가능해요. 아래는 연결을 해놓는 예시 파일입니당. participant와 domain이 연결되는 것 이외에도 추가적으로 좀 더 많은 것들이 진행되고 있는데 읽어보면 금방 알 수 있는 내용이예요.
// start all local instances defined in the configuration file
nodes.local.start()
// Connect participant1 to mydomain using the connect macro.
// The connect macro will inspect the domain configuration to find the correct URL and Port.
// The macro is convenient for local testing, but obviously doesn't work in a distributed setup.
participant1.domains.connect_local(mydomain)
// Connect participant2 to mydomain using just the target URL and a local name we use to refer to this particular
// connection. This is actually everything Canton requires and this second type of connect call can be used
// in order to connect to a remote Canton domain.
//
// The connect call is just a wrapper that invokes the `domains.register`, `domains.get_agreement` and `domains.accept_agreement` calls.
//
// The address can be either HTTP or HTTPS. From a security perspective, we do assume that we either trust TLS to
// initially introduce the domain. If we don't trust TLS for that, we can also optionally include a so called
// EssentialState that establishes the trust of the participant to the domain.
// Whether a domain will let a participant connect or not is at the discretion of the domain and can be configured
// there. While Canton establishes the connection, we perform a handshake, exchanging keys, authorizing the connection
// and verifying version compatibility.
participant2.domains.connect("mydomain", "http://localhost:5018")
// The above connect operation is asynchronous. It is generally at the discretion of the domain
// to decide if a participant can join and when. Therefore, we need to asynchronously wait here
// until the participant observes its activation on the domain. As the domain is configured to be
// permissionless in this example, the approval will be granted immediately.
utils.retry_until_true {
participant2.domains.active("mydomain")
}
participant2.health.ping(participant1)
기본적인 명령어들을 입력해보면서 Canton이 어떻게 동작하는 지 알 수 있게 되었어요. 이 다음부터는 조금 더 어려운 과정을 진행해보려고 해요.