Corda Core トランザクションフローのコードリーディング
前提知識
トランザクションに付与する署名条件とContractの条件, Vaultにトランザクションを保存するNodeの指定周りについて再整理
requiredSigners
トランザクションのCommandsに含めるPublickeyのList
FlowにてTransactionBuilder addCommandsする際に定義する
requiredSignersに含めた公開鍵の「署名がトランザクションに付与されているかどうか」を toLedgerTransactionの中のverifyRequiredSignatures()で検証している
この検証はContractによる状態遷移の検証とは別
participants
Stateのプロパティとして定義する
FinalityFlow sessionsに指定され, トランザクションを共有されたNodes(requiredSignerも含む)は, 受け取ったトランザクションが持つOutputsのうち, 自身がparticipantsに含まれているStateのみをVaultに保存する(デフォルト設定)
recordTransaction()でvaultにStateを保存する場合に, StatesToRecord(NONE, ALL_VISIBLE, ONLY_RELEVANT) がデフォルトではONLY_RELEVANTになっており, この場合に自身がparticipantsに含まれているStateのみがVaultに保存される
NotaryVaultService.ktの以下の部分のコードで分岐している
observable nodeが使用するReceiveRegulatoryReportFlowではALL_VISIBLEを使用してReceiveTransactionFlowを実行している
code:ReceiveRegulatoryReportFlow.kt
@InitiatedBy(ReportToRegulatorFlow::class)
class ReceiveRegulatoryReportFlow(private val otherSideSession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
subFlow(ReceiveTransactionFlow(otherSideSession, true, StatesToRecord.ALL_VISIBLE))
}
}
これによってobserver nodeは自身がstateのparticipantsに含まれないTXもVaultに保存するようになっている
Stateのプロパティなので定義しておくとContractの中で参照できる
"All of the participants must be signers." using (command.signers.containsAll(out.participants.map { it.owningKey })) のような形で「participantsがrequiredsignersに入ってるか」等を検証するコードがContractには書かれる
FinalityFlow sessions
FinalityFlowで指定する「トランザクションを共有するNodeの宛先」リスト
トランザクションを受け取ったNodeはReceiveFinalityFlow(ReceiveTransactionFlow)で受け取り, トランザクションの署名検証, Contractによる状態遷移の検証をトランザクションチェーンの全てのトランザクションに対して遡及的に行う
つまり, 「トランザクションに署名が必要なNode」と「トランザクションを共有するNode」は全く別で指定可能
Observer Nodeはここで指定するイメージ
トランザクションフローのコードリーディング
検証系メソッド
1. 署名の検証
requiredSignerの署名 / Notaryの署名を検証するmethods
checkSignaturesAreValid() 署名が正しいかのみ検証
verifyRequiredSignatures() 署名が正しいかだけではなくrequiredSignersの分集まっているか(マルチシグが満たされているか)も検証
これらを内部で実行している頻出method
toLedgerTransaction(services, checkSufficientSignatures)
(1). checkSufficientSignatures == false
checkSignaturesAreValid() 署名が正しいかのみ検証
(2). checkSufficientSignatures == true
verifyRequiredSignatures() 署名が正しいかだけではなくrequiredSignerの分集まっているか(マルチシグが満たされているか)も検証
verifySignaturesExcept(notarySigned) 引数に指定したPublickeyを除くrequiredSignersの署名が集まっているかを検証
2. 状態遷移の検証
Contract で状態遷移を検証するmethods (同時にContract自体の正当性を担保するContract Constraintsも検証)
TransactionVerifierServiceInternal.kt のverify()でContract Constraintsの検証とContractの実行
これを内部で実行している頻出method
LedgerTransaction.verify()
services.transactionVerifierService.verify(ltx)
Contract Constraints検証の詳細
code:TransactionVerifierServiceInternal.kt
fun verify() {
checkNoNotaryChange()
checkEncumbrancesValid()
// 1. . TXのAttachmentsに含まれているContract.jarのHashとClassnameに対応するClassをmappingする
val contractAttachmentsByContract = getUniqueContractAttachmentsByContract()
// 2. Attachmentsに指定されているContract.jarがContract Constraints(hash constraintsやsignature constraints)を満たすか検証
verifyConstraints(contractAttachmentsByContract)
// 3. StateごとにContract ConstraintがサポートされているConstraintsかどうか, またinputsとoutputsの全てのStateのContract Constraintsが対応しているかを検証
verifyConstraintsValidity(contractAttachmentsByContract)
// 4. 各Stateの@BelongsToContractアノテーションで指定されているContractと一致するかを検証
validateStatesAgainstContract()
// 5. Contractでトランザクションの状態遷移を検証する
verifyContracts()
}
Etaro.icon > このContract ConstraintsがAttachmentによって保証されるという点が, Attachmentが重要な理由
署名の検証 + 状態遷移の検証
署名の検証と状態遷移の検証をどちらも内部で行うmethod
SignedTransaction.verify(serviceHub, checkSufficientSignatures)
内部で toLedgerTransaction(services, checkSufficientSignatures)、services.transactionVerifierService.verify(ltx) を実行
Etaro.icon> 順番的には署名の検証をしてから状態遷移の検証をしている
あるTXのトランザクションチェーンの訴求的検証
ResolveTransactionsFlow(it, otherSideSession, statesToRecord)
resolver.downloadDependencies(batchMode) 訴求的に足りないdependenciesを貰いに行くback-chain fetchingを行う
resolver.recordDependencies(usedStatesToRecord)
内部で再起的に SignedTransaction.verify()を実行
DbTransactionsResolverの中のrecordDependenciesを呼び出してトランザクションチェーンのトランザクションをvaultに保存することも行う
code:DbTransactionsResolver.kt
override fun recordDependencies(usedStatesToRecord: StatesToRecord) {
val sortedDependencies = checkNotNull(this.sortedDependencies)
logger.trace { "Recording ${sortedDependencies.size} dependencies for ${flow.txHashes.size} transactions" }
val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage
for (txId in sortedDependencies) {
// Retrieve and delete the transaction from the unverified store.
val (tx, isVerified) = checkNotNull(transactionStorage.getTransactionInternal(txId)) {
"Somehow the unverified transaction ($txId) that we stored previously is no longer there."
}
if (!isVerified) {
tx.verify(flow.serviceHub)
flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx))
} else {
logger.debug { "No need to record $txId as it's already been verified" }
}
}
}
あるTXと, そのトランザクションチェーンの訴求的検証
ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord)
内部で SignedTransaction.verify(serviceHub, checkSufficientSignatures) も ResolveTransactionsFlow(it, otherSideSession, statesToRecord)も行う
serviceHub.recordTransactions(statesToRecord, setOf(stx))で受け取ったTXもvaultに保存する
トランザクションチェーンのトランザクションもResolveTransactionFlowで保存する
更新系メソッド
1. Transactionテーブル
トランザクションをひたすらappendしていくテーブル
2. Vaultテーブル
Vault TableはStateが入ってる場所で, vaultQuery()をなげるところ
Transaction テーブルの状態更新をトリガーにそのトランザクションからStateを取り出してUpdateされる
StatesToRecord.NONEであれば保存しない
StatesToRecord.ONLY_RELEVANTであれば自身がparticipantsに含まれるStateのみ保存
StatesToRecord.ALL_VISIBLEであれば全部保存
ResolveTransactionFlow
dependenciesトランザクションのみをTransactionテーブルに全て保存する
Transactionテーブルの更新にトリガーされて, Vaultテーブルの更新が走るが、この時どのStateを保存するかは引数に渡したStatesToRecordで決定する
RecieveTransactionFlow
ResolveTransactionFlowをStatesToRecord.ONLY_RELEVANTで実行する
それに加えて
StatesToRecord.ONLY_RELEVANTで受け取ったトランザクションもrecordTransactionでトランザクションテーブルに保存する
トランザクションフロー
1. Initiatorがトランザクション作成/検証
InitiatorFlow
TransactionBuilder.verify()
toLedgerTransaction(services)
LedgerTransaction.verify()
TransactionVerifierServiceInternal.kt のverify()でContract Constraintsの検証とContractの実行
2. InitiatorがRespondersから署名を集めに行く
CollectSignatureFlow
partiallySignedTx.verifySignaturesExcept(notarySigned)
自身の分の署名が集まっているか検証
partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify()
TransactionVerifierServiceInternal.kt のverify()でContract Constraintsの検証とContractの実行
3. Responderが受け取ったトランザクションに署名
SignTransactionFlow
ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false)
A. SignedTransaction.verify(serviceHub, checkSufficientSignatures)
verifyRegularTransaction(services, checkSufficientSignatures)
a. toLedgerTransaction(services, checkSufficientSignatures)
checkSignaturesAreValid() 署名が正しいかのみ検証
b. services.transactionVerifierService.verify(ltx).getOrThrow()
verifier.verify()
TransactionVerifierServiceInternal.kt のverify()でContract Constraintsの検証とContractの実行
B. ResolveTransactionsFlow(it, otherSideSession, statesToRecord)
resolver.downloadDependencies(batchMode) 遡及的に足りないdependenciesを貰いに行くback-chain fetchingを行う
resolver.recordDependencies(usedStatesToRecord)
tx.verify(flow.serviceHub) SignedTransaction.verify()で検証
verifyRegularTransaction(services, checkSufficientSignatures)
toLedgerTransaction(services, checkSufficientSignatures)
checkSignaturesAreValid() 署名が正しいかのみ検証
services.transactionVerifierService.verify(ltx).getOrThrow()
verifier.verify()
TransactionVerifierServiceInternal.kt のverify()でContract Constraintsの検証とContractの実行
4. Initiatorが, Responderの署名付きトランザクションを受け取る
CollectSignatureFlow
if (notaryKey != null) stx.verifySignaturesExcept(notaryKey) else stx.verifyRequiredSignatures()
署名集めてきた結果、requiredSigners - notaryの分の署名が集まっているか検証
5. InitiatorがNotarisationRequestを投げる
FinalityFlow
verifyTx()
if (notary != null) transaction.verifySignaturesExcept(notary.owningKey) else transaction.verifyRequiredSignatures()
val ltx = transaction.toLedgerTransaction(serviceHub, false).verify()
Etaro.icon > memo:
第二引数falseなのでcheckSignaturesAreValid() 署名が正しいかのみ検証
TransactionVerifierServiceInternal.kt のverify()でContract Constraintsの検証とContractの実行
val notarised = notariseAndRecord()
subFlow(NotaryFlow.Client(transaction, skipVerification = true))
skipVerificationをtrueにすると以下の部分の検証(requiredSigners - notaryの分の署名が集まっているか検証)を実行しなくなる. これは4. において既に実行しているからである.
code:NotaryFlow.kt
if (!skipVerification) {
// TODO= CORDA-3267 Remove duplicate signature verification stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
stx.verify(serviceHub, false)
}
val response = notarise(notaryParty)
val requestSignature = generateRequestSignature()
notarisationRequest.generateSignature(serviceHub)
keyManagementService.sign(serializedRequest, myLegalIdentity) →signature
code: NotaryFlow.kt
return if (isValidating(notaryParty)) {
sendAndReceiveValidating(session, requestSignature)
} else {
sendAndReceiveNonValidating(notaryParty, session, requestSignature)
}
val payload = NotarisationPayload(stx, signature)
subFlow(NotarySendTransactionFlow(session, payload))
Notaryにはpayloadを送信している
6. NotaryがNotarisationRequestを受け取って検証/署名してInitiatorに返す
NotaryServiceFlow.kt
val tx: TransactionParts = validateRequest(requestPayload) で送信されてきたpayloadをvalidation
code: NotaryServiceFlow.kt
checkNotary(transaction.notary)
checkParameterHash(transaction.networkParametersHash)
checkInputs(transaction.inputs + transaction.references)
val request = NotarisationRequest(tx.inputs, tx.id)
validateRequestSignature(request, requestPayload.requestSignature)
notarisation requestについてる署名が送信してきたpeerのものかを検証
verifyTransaction(requestPayload)
code:NonValidatingNotaryFlow.kt
is FilteredTransaction -> {
tx.apply {
verify()
checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
checkAllComponentsVisible(ComponentGroupEnum.REFERENCES_GROUP)
if (minPlatformVersion >= 4) checkAllComponentsVisible(ComponentGroupEnum.PARAMETERS_GROUP)
}
code:ValidatingNotaryFlow.kt
@Suspendable
override fun verifyTransaction(requestPayload: NotarisationPayload) {
try {
val stx = requestPayload.signedTransaction
resolveAndContractVerify(stx)
verifySignatures(stx)
} catch (e: Exception) {
throw NotaryInternalException(NotaryError.TransactionInvalid(e))
}
}
@Suspendable
private fun resolveAndContractVerify(stx: SignedTransaction) {
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
stx.verify(serviceHub, false)
}
private fun verifySignatures(stx: SignedTransaction) {
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
checkSignatures(transactionWithSignatures)
}
private fun checkSignatures(tx: TransactionWithSignatures) {
tx.verifySignaturesExcept(service.notaryIdentityKey)
}
ここで行っている検証としては
Non-validating Notary
Transaction Merkle Treeの検証
Inputs, Time-windows, referencesがvisibleかどうか
notaryが自分かどうか, Whitelistedかどうか
のみを検証している
Validating Notary
resolveAndContractVerify(stx)においてResolveTransactionFlowとSignedTransaction.verify()をcheckSufficientSignatures=falseで実行している
ReceiveTransactionFlowのcheckSufficientSignatures=falseでの実行と同等の処理
つまり, 「署名が正しいかのみ検証」と「Contract Constraintsの検証とContractの実行による状態遷移の検証」の2つをトランザクションとトランザクションチェーン全てに行っている
verifySignatures(stx)においてtx.verifySignaturesExcept(service.notaryIdentityKey)を実行
つまり, requiredSigners(Notary以外)の署名が揃っているかの検証も行っている
Validating Notaryは, ResolveTransactionFlowを行っており, その中でDBTransactionStorage.addTransaction()を実行してトランザクションチェーンの全てのdependenciesトランザクションをTransactionテーブルにAppendしている
これをトリガーにNodeVaultServiceが発火されるが, StatesToRecord.NONEで実行しているため、Vaultsテーブルには何も保存されない
Etaro.icon > 結果としてValidating NotaryはTransaction Tableにトランザクションは持つが、StateをVault Tablesには持たない(Vault Tablesは消費する目的のStateを入れるTableであるため) Etaro.icon > ここで面白いのは後述するNodeはReceiveTransactionFlowを実行して受け取ったトランザクションとdependencies両方をTransactionテーブルに保存するが、validating Notaryはdependenciesだけしか保存しない
Etaro.icon > Non-validating Notaryはなぜreference statesを受け取るのか。Notaryはなぜreference statesを保持するのか
A. トランザクションに参照データを含めたい場合があり、その参照データの最新バージョンであることを確認したい場合のためのもの. 例えば, 取引ブラックリストを参加者間で共有したいような場合に, 常にreference statesにブラックリストを入れてトランザクションのやりとりをすることでContractでreference stateを参照してvalidationできるのでブラックリストNodeとのトランザクションはエラーになる. このreference statesはトランザクションのinputにしてupdate(新しいノードを追加等)することもでき, 一度でも更新された古いreference statesを使用したTXはContract検証は通ってもNotaryが弾いてくれる. 更新したreference statesはNode間で直接もらいに行くFlowを用意しておく. Notaryは, 一度でもトランザクションのinputに含められて更新されたreference stateが入っているTXには「このreference statesは最新じゃないよ」として署名しないという検証ルールでvalidationしている.
service.commitInputStates(tx.inputs, tx.id, otherSideSession.counterparty, requestPayload.requestSignature, tx.timeWindow, tx.references)
val result = callingFlow.await( CommitOperation(this, inputs, txId, caller, requestSignature, timeWindow, references))
service.uniquenessProvider.commit(inputs, txId, caller, requestSignature, timeWindow, references).toCompletableFuture()
serviceのuniquenessProviderは今回Raft版ではなくPersistentUniquenessProvider.ktであるとすると
code:PersistentUniquenessProvider.kt
override fun commit(
states: List<StateRef>,
txId: SecureHash,
callerIdentity: Party,
requestSignature: NotarisationRequestSignature,
timeWindow: TimeWindow?,
references: List<StateRef>
): CordaFuture<UniquenessProvider.Result> {
val future = openFuture<UniquenessProvider.Result>()
val request = CommitRequest(states, txId, callerIdentity, requestSignature, timeWindow, references, future)
requestQueue.put(request)
log.debug { "Request added to queue. TxId: $txId" }
return future
}
code: PersistentUniquenessProvider.kt
private fun commitOne(
states: List<StateRef>,
txId: SecureHash,
callerIdentity: Party,
requestSignature: NotarisationRequestSignature,
timeWindow: TimeWindow?,
references: List<StateRef>
) {
database.transaction {
logRequest(txId, callerIdentity, requestSignature)
val conflictingStates = findAlreadyCommitted(states, references, commitLog)
if (conflictingStates.isNotEmpty()) {
if (states.isEmpty()) {
handleReferenceConflicts(txId, conflictingStates)
} else {
handleConflicts(txId, conflictingStates)
}
} else {
handleNoConflicts(timeWindow, states, txId, commitLog)
}
}
}
code:PersistentUniquenessProvider.kt
private fun handleReferenceConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
if (!previouslyCommitted(txId)) {
val conflictError = NotaryError.Conflict(txId, conflictingStates)
log.info("Failure, input states already committed: ${conflictingStates.keys}. TxId: $txId")
throw NotaryInternalException(conflictError)
}
log.info("Transaction $txId already notarised. TxId: $txId")
}
private fun handleConflicts(txId: SecureHash, conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>) {
if (isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
log.info("Transaction $txId already notarised. TxId: $txId")
return
} else {
log.info("Failure, input states already committed: ${conflictingStates.keys}. TxId: $txId")
val conflictError = NotaryError.Conflict(txId, conflictingStates)
throw NotaryInternalException(conflictError)
}
}
private fun handleNoConflicts(timeWindow: TimeWindow?, states: List<StateRef>, txId: SecureHash, commitLog: AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef>) {
// Skip if this is a re-notarisation of a reference-only transaction
if (states.isEmpty() && previouslyCommitted(txId)) {
return
}
val outsideTimeWindowError = validateTimeWindow(clock.instant(), timeWindow)
if (outsideTimeWindowError == null) {
states.forEach { stateRef ->
}
val session = currentDBSession()
session.persist(CommittedTransaction(txId.toString()))
log.info("Successfully committed all input states: $states. TxId: $txId")
} else {
throw NotaryInternalException(outsideTimeWindowError)
}
}
code:PersistentUniquenessProvider.kt
private fun respondWithSuccess(request: CommitRequest) {
val signedTx = signTransaction(request.txId)
request.future.set(UniquenessProvider.Result.Success(signedTx))
}
code: SinglePartyNotaryService.kt
fun signTransaction(txId: SecureHash): TransactionSignature {
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
return services.keyManagementService.sign(signableData, notaryIdentityKey)
}
上記のmethodsでNotaryのDBにcommitされる
この時, Input StateRefがConflictしないか(消費済みInputではないか), referencesがconflictしないか(古いreferencesを見ていないか)を検証している
CommitがSuccessすると, signTransactionによって生成されたsignedTxがsignatureにsetされたUniquenessProvider.Resultを返す
Notaryが使用しているsignTransactionでは, TxIdに加えてCorda NodeのversionであるplatformVersionと 使用している署名スキームであるsignatureSchemaから成るSignableDataに署名されている
Notaryが自身のvaultにcommit
Etaro.icon > 上記methodはValidating, Non-validating 共通
sendSignedResponse(transactionId!!, commitStatus.signature)
commitStatus.signatureとtxIdをInitiatorに返却する
tomoaki.icon > 返却先はどこで指定しているのだろうか
Q. Notaryは何に署名して, 何をinitiatorに返しているのか
A. SignableData(txId, platformVersion, signatureSchema)に秘密鍵で署名したTransactionSignatureを返す
Q. Non-validating NotaryとValidating Notaryはそれぞれどんなデータを保持するのか
A. Non-validating NotaryはInputsとReferencesのHashがひたすら積まれる二重支払い検証用のテーブルとrequesting peer。Validating Notaryはそれに加えて, Transaction Tablesにトランザクションデータを持つ. (Vaultテーブルは空)
7. Initiatorが受け取って検証し, Respondersに配る
FinalityFlow
検証
NotaryFlow
validateResponse(response, notaryParty)
it.validateSignatures(stx.id, notaryParty) ← NotaryからのResponseにNotary公開鍵の署名がついているか検証
serviceHub.recordTransactions(statesToRecord, listOf(notarised)) NotaryからのResponseを検証したらそれらのトランザクションを特に検証せず, Vaultに保存する
ここではcordappのFlowの中でFinalityFlowの引数statesToRecordに何も入れないで実行しており, defaultではONLY_RELEVANTになり, 自身がparticipantsに入っているStateのみしか保存しない
Etaro.icon < NotaryからのresponseをrecordTransactions時に検証しないのは, validateSignatures時に自分がNotarisationRequestにのせたTx IDに署名がついてることを検証しているから. Tx IDはトランザクションの各要素をleafとしたmerkle treeであるためこれで検証完了している
配布
subFlow(SendTransactionFlow(session, notarised))
定番のSendTransactionFlow()で配る
val notarised = notariseAndRecord()
code:FinalityFlow.kt
@Suspendable
private fun notariseAndRecord(): SignedTransaction {
val notarised = if (needsNotarySignature(transaction)) {
progressTracker.currentStep = NOTARISING
val notarySignatures = subFlow(NotaryFlow.Client(transaction, skipVerification = true))
transaction + notarySignatures
} else {
logger.info("No need to notarise this transaction.")
transaction
}
logger.info("Recording transaction locally.")
serviceHub.recordTransactions(statesToRecord, listOf(notarised))
logger.info("Recorded transaction locally successfully.")
return notarised
}
notarised = SignedTransaction + notarySignatures になる
8. Respondersが受け取って検証し, 自身のvaultに保存
ReceiveFinalityFlow
code: FinalityFlow.kt
override fun call(): SignedTransaction {
return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) {
override fun checkBeforeRecording(stx: SignedTransaction) {
require(expectedTxId == null || expectedTxId == stx.id) {
"We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" +
"not recorded and nor its states sent to the vault."
}
}
})
}
ReceiveTransactionFlowを実行している
つまり, トランザクションチェーン全体を遡及的に 「署名が正しいか/requiredSigners条件を満たしているか」と「Contract Constraintsを満たしているか/状態遷移がContractに従っているか」をvalidationしている
Etaro.icon > ここでは ReceiveTransactionFlowのcheckSufficientSignatures = trueであることに注意
ReceiveTransactionFlowの中でResolveTransactionFlowを実行しているため, トランザクションチェーンのdependenciesトランザクションは全てTransaction テーブルに保存される
また, statesToRecord = ONLY_RELEVANTがデフォルト引数になっているため, CordappのResponderFlowの中でReceiveFinalityFlowを呼び出すときに明示的にstatesToRecordを指定しない限りは, 共有されたトランザクションのうち自身がparticipantsに含まれるStateのみしかvaultには保存しない
Notary Change Transaction
Stateは, 自信を生成したトランザクションにおいてNotary clusterを指定されており, 既存のStateのnotaryを書き換えることはできないような検証が行われている.
そのため, あるUTXOを使用しようとする場合, その指定されてNotary Clusterを使用しなければならない
しかし, privacyやperformanceの懸念がある場合や, 指定されたNotary Clusterの異なる複数のUTXOをInputにしたトランザクションをNotaryに署名requestしたいという場合などNotary Clusterを変更したい場合がある
この時に実行するのが Noatry Change Transaction
NotaryChangeFlowで実行され、トランザクションの構造としてはinputにnotary clusterを変更したいState, outputにnotary の指定だけを変更したstateが入る.
これを既存のNotary clusterに投げることによって, 既存のNotary clusterのvaultではこのUTXOは消費済みになり, 二重支払いを防止する