Corda4.7 Token SDKとAccount機能を利用した一括「re-issuance」(再発行)
■目次
○はじめに
○再発行の一括化
○性能比較
○実装方法
○まとめ
○おわりに
○はじめに
今回の記事は、Corda4.7で新しく追加された機能「re-issuance」についてです。「re-issuance」は長くなったトランザクションチェーンを切り、Stateを再発行する機能です。また、「re-issuance」(以下、「再発行」と表記)についてはSBI R3 Japanの記事もありますので、そちらをご参照ください。
SBI R3 Japan Support
https://support.sbir3japan.co.jp/hc/ja/articles/1500002504322-re-issuance%E3%82%92%E3%82%82%E3%81%A1%E3%81%84%E3%81%9FState%E3%81%AE%E5%86%8D%E7%99%BA%E8%A1%8C%E6%A9%9F%E8%83%BD
「re-issuanceをもちいたStateの再発行機能」(2021/06/01 閲覧)
今回のToken SDKとAccount機能と一括化について調査した動機は「Token SDKとAccount機能を利用したアプリで再発行機能を使用する際に参考となるサンプルがgithub上にはない」ということ、「再発行には複数のStepを要するためそれを一括で行いたい」という2点です。
したがって今回はToken SDKとAccount機能を利用した一括再発行を行う方法について検証しましたのでご紹介いたします。
○再発行の一括化
再発行のワークフローには以下4つの工程があります。
Step1:再発行リクエスト
Step2:再発行承認
Step3:元のStateの消費
Step4:Stateのロック解除
それぞれのStep毎にFlowがあり、手動でFlowを実施しなければなりません。本当にチェーンを切って良いか管理者が確認するという運用が必要であれば、この方法で実施すべきですが、実際の業務運用を考えるとすべて一括で実施したい要件も出てきます。また、日付が変わるタイミングで再発行可能分を一括で再発行するという要件があった際も同様です。したがって今回は1つのFlowで再発行可能なStateやTokenを一括で実行するFlowを実装します。一括化Flowのプロセスは以下の通りです。
1 Tokenの現在の所有者(以下requester)が一括化Flowを実施
2 検索処理等
3 再発行リクエスト
4 再発行時のHashの送受信
5 再発行承認
6 承認時のHashの送受信
7 Stateの消費
8 ロックの解除
再発行承認はrequesterと異なる者が行う必要があるため、一括化Flowに受信Flowを用意し、そこで承認Flowを実施する形式で行います。ワークフローは以下の通りです。
赤字に関しては、re-issuanceのライブラリで用意されています。(実際にはFlowをラップして利用しています。)
2:検索処理等では、再発行リクエストに必要なissuerやrequesterの情報検索や必要な形式にデータ加工を行います。issuerは再発行の承認者のため、今回はこのTokenを生成したTxのInitiator(≒前の holder)を指定します。
4:sendしたリクエスト時のHashは、issuerが受け取りそれをもとにStateを検索して再発行承認時に使用します。
6:承認時のHashをrequesterにsendします。これはロック解除に使用します。ここでの通信はFlowSessionのsend()メソッドとreceive()メソッドを利用します。
再発行はrequesterとissuerを一人ずつ指定するため、一度に再発行可能な処理数に大きく影響します。
下記の図では、B(requester)の保有するTokenを再発行します。送信元が両者ともA(issuer)のため、tx1で生成されたBTokenとtx2で生成されたBTokenを再発行Flowで一括実行することが可能です。
しかし、下記の図ではissuerがそれぞれ異なるため、1つずつ再発行Flowを実施する必要があります。
したがってrequesterとissuerのペア数だけ繰り返すことですべての再発行を実現します。そのため、実装上ではrequesterとissuerのペアとそれに紐づくState(Token)をフィルタリングしておく必要があります。
○TokenとAccountを利用したアプリの再発行
Account機能は基本的に取引を行う度にRequestKeyForAccountを呼び、新しい公開鍵を生成します。つまり下記の図のようにアカウントは同じであってもすべて公開鍵が異なります。したがって前項の図2のパターンと同じでもAccount機能を利用した際にはTxごとに再発行が必要です。この実装の場合、1つのTokenにつき再発行のFlowを実施する必要があるため大幅にTx数が増えてしまいます。
そこで、取引の度にRequestKeyForAccountを呼ばず、Account生成時に一度だけ呼び、今後の取引はAccount生成時に作った公開鍵を利用します。これは、どれだけ取引を行ってもアカウントに対しての一つの公開鍵を利用するのでアカウントごとに公開鍵が同じものになります。
○性能比較
今回実装したRequestKeyForAccountを呼ぶ方法(以下、「公開鍵を都度生成」と表記)と呼ばない方法(以下、「公開鍵を再利用」と表記 )の二種類の実装において、Tx数とレコード数、処理時間をそれぞれ比較します。
・アプリの概要
Tokenをやり取りするアプリは、NodeA、NodeB、NodeC、Notaryの4つのノードで、ノードがAccountに対してTokenを発行し、Account同士でTokenをやり取りします。またAccount数は比較ケースにおける最低数で作成します。
・比較ケース
TxチェーンのTxは「発行→前のholder→今のholder」の3つです。
再発行件数を1件と10件とで 比較します。
大文字アルファベットがノードで、小文字アルファベットがそれに属するアカウントです。
下記の図を例として流れを解説します。
tx1:NodeAがa1にTokenを発行
tx2:a1がa2にTokenを送信
tx3:再度a2がa1にTokenを送信
ここでは発行したTokenすべて送信します。10件の場合、tx1~tx3を10回繰り返します。最後にTokenを受け取ったAccount(a1)がrequesterとして再発行を行います。
下記が性能の比較結果になります。表の見方としては、表の一番上のパターンは上記の図のようなパターンになります。この時の総生成Tx数は再発行前後のnode_transactionテーブルのレコード増加数を示しています。StateとToken数ではvault_stateテーブルのレコード増加数を示しています。黄色の背景の箇所に着目してみていきます。1件ずつのパターンではほぼ差は見られません。総生成Tx数の内訳として再発行リクエスト、再発行承認、Tokenの消費 、ロック解除の4件のトランザクションです。StateとToken数の内訳はリクエストStateが1件、ロックStateが2件、コピーTokenが2つの計5件になります。
10件ずつのパターンではそれぞれ大きく差が見られます。
公開鍵を再利用するパターンでは、総生成Tx数は1件の時と変わらず、StateとToken数は5件から23件に増えています。StateとToken数の内訳はリクエストStateが1件、ロックStateが2件、コピーTokenが20件の計23件になります。つまり、公開鍵を使いまわすパターンでは、何件再発行しても、リクエストStateが1件、ロックStateが2件で数は増えることはなくコピーTokenのみがToken1件につき2件増えます。
公開鍵を都度生成するパターンでは総生成Tx数40件、StateとToken数は50件になっています。これは1件の時の数がそのまま10倍になっています。つまり公開鍵を都度作成するパターンでは、Token1件につきリクエストStateが1件、ロックStateが2件、コピーTokenが2件で5件増加します。
Tx数が大きく増加し、それに伴い署名数も増加し処理時間などの性能にも影響を与えます。
・性能結果
現段階ではTokenとアカウントを利用する場合においては公開鍵を再利用する方法がTx数や処理速度から考えるとベストのように見えます。(※後述の【公開鍵を都度生成する方法でのTx数について】参照)
○実装方法
Token SDKとAccount機能を用いた再発行の実装について公開鍵を再利用する方法の解説をしていきます。
ここでは最初に紹介した図1の順番に説明していきます。
1.一括のFlow実施
ここでのパラメータはissuer、再発行対象のStateRefのStringのList、requesterのnameの3つ受けとります。(このフローを呼び出す前に再発行対象のStateRefとrequesterとissuerとを紐づける処理を行ってそれらをパラメータとしてこのFlowを呼び出しています。)
@InitiatingFlow
@StartableByRPC
class RequestReissueIssuedTokenState(
private val issuerTxAccount: AbstractParty,//issuer
private val stateRefStringsToReissue: List<String>,//再発行対象のStateRefのStringのList
private val requester: String
) : FlowLogic<SecureHash>() {
2.検索処理
1で受け取ったStringのStateRefのListからIndexのListとStateRefのListを作成します。再発行はすべて同じTokenTypeである必要があるので再発行対象のTokenはすべて同じTokenTypeという前提で一つだけ取り出します。issuerのSessionを確立して、issuerのAccountInfoを受け取ります。このAccountInfoからissuerのhostのノードを取得します。ここでは鍵をもとにAccoountのPartyを生成していますが、issuerTxAccountをそのまま利用しても構いません。
requester側ではnameからrequesterのAccountInfoを取得し、そこからAccoountの鍵のListを取得します。公開鍵を再利用しているため、絶対に鍵は1つだけになり、それをもとにAccoountのPartyを生成します。
@Suspendable
override fun call(): SecureHash {
val stateRefStringsToReissueIndexList =
stateRefStringsToReissue.indices.toList()
val stateRefsToReissue =
stateRefStringsToReissue
.map { parseStateReference(it) }
//ここからTokenType取得
val reissuedTokenType =
serviceHub
.toStateAndRef<FungibleToken>(
stateRefsToReissue.first()
).state.data.issuedTokenType
//issuerTxAccountのPartyを取得
val issuerSession = initiateFlow(issuerTxAccount)
val issuerInfo =
issuerSession
.sendAndReceive<AccountInfo>(
issuerTxAccount.owningKey
).unwrap { it }
val issuer = AnonymousParty(issuerTxAccount.owningKey)
//requesterのPartyを取得
val requesterInfo =
accountService
.accountInfo(requester).single().state.data
val requesterKeyList =
accountService
.accountKeys(requesterInfo.identifier.id)
val requesterParty = AnonymousParty(requesterKeyList.single())
3.再発行要求
再発行要求のFlowを呼び出します。ここでのパラメータにはissuer、再発行対象のStateRefのList、Tokenの発行コマンド、追加の署名者、requesterになります。Tokenの発行コマンドには2で取得したTokenTypeと2で作成したIndexのListを渡します。また、issuerがAccountの場合、追加の署名者にhostを入れる必要があります。今回、requesterはAccoountなので設定します。
//Step1再発行要求を実施
println("[Start RequestReIssuance]")
val requestHash =
subFlow( RequestReissuanceAndShareRequiredTransactions<FungibleToken>(
issuer = issuer,
stateRefsToReissue = stateRefsToReissue,
assetIssuanceCommand = IssueTokenCommand(
reissuedTokenType,
stateRefStringsToReissueIndexList
),
extraAssetIssuanceSigners = listOf(
//issuerがaccountの場合はhostを入れる
issuerInfo.host
),
requester = requesterParty
)
)
4.再発行要求のHashの送受信
・requester側
ここでrequesterは再発行要求Hashをsendし、再発行承認のHashの受信待ちになります。
//再発行要求のtxidを承認者に送り再発行承認のhashを受け取る
val syouninHash =
issuerSession
.sendAndReceive<SecureHash>(requestHash).unwrap { it }
・issuer側
2でAccountInfoをsendし、再発行要求Hashの受信待ち状態であるので、ここでHashを受け取り動き始めます。それをもとに、再発行承認用にデータを加工します。
@InitiatedBy(RequestReissueIssuedTokenState::class)
class RequestReissueIssuedTokenStateAccepter(
val otherSession: FlowSession
) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
/*一部省略*/
//アカウント情報送り、再発行要求のHashを受け取る。
val requestHash =
otherSession
.sendAndReceive<SecureHash>(accountInfo)
.unwrap { it }
val stateAndRef =
serviceHub
.toStateAndRef<ReissuanceRequest>(
StateRef(requestHash, 0)
)
5.再発行承認
4で加工したデータをパラメータに再発行承認を実施します。
//Step2再発行承認
println("[Start Reissuance]")
val syoninHash = subFlow(
ReissueStates<ReissuanceRequest>(
stateAndRef
)
)
6.再発行承認のHashの送受信
・issuer側
再発行承認のHashをrequesterにsendします。
otherSession.send(syoninHash)
・requester側
4の箇所で再発行承認のHashを受信します。
7.元のState(Token)の消費
Tokenの消費は一から自分で実装する必要があります。また、再発行するToken(State)は消費する時にissuerも署名する必要があるためissuerもパラメータに渡します。
//Step3コピー元のStateの消費
println("[Start DeleteState]")
val deleteHashes = subFlow(
DeleteIssuedTokenStateByRef(stateRefStringsToReissue,issuer)
)
8.ロックの解除
6で受信した再発行承認のHashを加工します。第一引数は、ロックがかかったコピーTokenのStateRefのStringのListで第二引数は、ロックStateのStateRefのString、第三引数はもとのTokenの消費時のHashのListです。このFlow中で再度データを加工し、MoveTokenCommandを作成し、実際のロック解除のFlowを実施します。
//Step4コピーされたStateのLock解除
println("[Start Unlock]")
val unLockHashStringList =
stateRefStringsToReissueIndexList.map {"$syouninHash($it)"}
return subFlow(
UnlockIssuedTokenState(
unLockHashStringList,
"${syouninHash}(${unLockHashStringList.size})",
deleteHashes
)
)
○まとめ
性能比較及び実装をしていく中で再発行条件や注意点がわかりましたので以下にまとめます。
【再発行条件】
・Tokenのholderと再発行要求のrequesterは同じ公開鍵を利用する必要がある。
・複数Tokenを一度で再発行するときはholder全員が同じ鍵を使って取引する必要がある。
・再発行のissuerとrequesterは鍵が異なる必要がある。
・issuerは再発行対象のState(Token)消費時に署名をする必要がある。
・再発行のissuerは第三者でも可能。
【実装時の注意点】
▼公開鍵を都度生成する方法での実装における注意点
・再発行要求時にRequestKeyForAccountを行うとholderとrequesterの鍵が異なるため行いません。
・issuerを第三者にした際は、再発行対象Tokenの関係者のアカウントの公開鍵をissuerに同期させる必要があります。
▼共通の実装における注意点
・一括Flowは1つのラップFlow内で複数のFlowを呼び出し、それぞれでState(Token)操作を行っており、いずれかのFlowが失敗した時に再実行が難しくなるため各々のリカバリ方法を検討する必要があります。
・実際にissuerに第三者を指定することは可能ですが、プライバシーの観点からも第三者に再発行のissuerに指定することは必要なTxなどを渡す必要があるため望ましくありません。
【公開鍵を都度生成する方法でのTx数について】
公開鍵を都度生成する場合、Tx数が増えてしまい公開鍵を再利用する方法がよいと性能比較の結果から述べましたが、実際には上記の再発行条件を踏まえて実装すると大幅に減らすことができます。それは再発行前に自分自身宛に、再発行対象のTokenを一度のTxで全て送ります。このようにすることで、再発行対象のTokenのholderの鍵はすべて同じものになります。再発行の前にこのような下処理を行ってから再発行を行うことで公開鍵を再利用する場合と同程度までTx数を抑えることができます。
【まとめ】
公開鍵を再利用する方法では同じ鍵を再利用するためセキュリティ面での懸念があります。公開鍵を都度生成する方法ではTokenをやり取りする度にその時の鍵を相手に同期する処理があるので、取引が増えるほどスループットの低下につながります。また、前者に比べてDBの使用量も高くなります。したがって実装するにあたり、どちらにも長所短所があるため要件に合わせて実装していくことが望ましいと思います。
○おわりに
今回、Token SDKとAccoount機能を用いての再発行を実装するにあたって、署名周りのエラーに多く躓きました。必要な箇所で署名が足りていないことや余計な署名を要求してしまっていたことなど、とても苦労しました。また、チェーンを切って再発行するため、以前記事にしたArchive Serviceとも相性がいいと思うので機会があれば併せて実装してみたいと思います。
今回は以上になります。
お問い合わせ: bc_prom@ml.tis.co.jp
記:TIS Blockchain Promotion Office (Hikaru Suzuki)
Thanks to Kiyotaka Yamasaki , Hideki Nakachi and Naoko Kanto.