まえがき
今回、Reference State(以下Ref.State)に関する細かな動作確認を行うため調査を行いました。
調査にあたりましてサンプルプログラムを作成しました。コード解説編と デモンストレーション編の2部編成でお届けしたいと思います。本記事はRef.Stateについて詳しく知りたいCordaに精通していらっしゃるエンジニアの方やセールスの方を対象としております。 Cordaについて詳しく知りたい方は R3公式ドキュメント や 株式会社digglueさんの記事 をご覧ください。また、Cordaの概念であるUTXOモデルについて知りたい方は、こちらの解説記事 をご覧ください。
自己紹介
SBI R3 Japan株式会社にインターンでお邪魔している井本翔太と申します。現在、都内の大学に通いながらエンジニアとして働かせていただいています。昨年からブロックチェーンに興味を持ち始め、その仕組みや活用方法を学ぶ過程でCordaと出会いました。私にとってCordaは社会にブロックチェーンという存在を浸透させる上で大きな役割を果たすと考えております。
Ref.Stateについて
Ref.Stateとは参照情報を表すStateの事で、使用時はトランザクションに含まれます。
Ref.Stateは、含まれるトランザクションで消費されることはありませんが、Ref.State自体の更新では消費されます。
調査内容
調査1:Ref.Stateをトランザクションに含め、コントラクトで検証可能か
調査2:あるノードで発行したRef.Stateのバックチェーンを別のノードで確認できるか
調査3:消費済みのRef.Stateをトランザクションに含められるか
調査結果について
調査1,2については要望を満たすことができましたが、調査3はエラーが出力されました。
調査3で出力されたエラーについてはデモンストレーション編にて説明したいと思います。
※調査1,2と調査3では実装が異なるため、サンプルプログラムは2つ作成しました。
サンプルプログラムについて
サンプルプログラムはSBI R3 Japan株式会社が提供しております Corda trainingのコードにAddressState(住所録)をRef.Stateとして加えました。Corda trainingで扱うプログラムはIOU(借用証書)の発行(Issue)、譲渡(Transfer)、返済(Settle)に関するプログラムになります。今回は必要最低限の機能を実装するためにRef.Stateの実装対象をIssueのみに絞りました。
ファイルの説明
新規作成ファイル
-
AddressState.java:
- Ref.Stateに相当するStateになります
-
AddressContract.java:
- AddressStateを発行するPublishコマンドと更新するMoveコマンドを定義し、
それらに関する制約を加えました。
- AddressStateを発行するPublishコマンドと更新するMoveコマンドを定義し、
-
PublishFlow.java:
- Publishに関するFLowを作成しました。
-
MoveFlow.java:
- Moveに関するFlowを作成しました。
既存のファイルの変更
-
IOUIssueFlow.java:(調査1,2)
- 最新のAddressState(=Ref.State)をトランザクションに含める処理を追加
-
IOUContract.java:
- AddressState(=Ref.State)の検証に関する制約を追加
-
IOUTransferFlow:
- AddressState(=Ref.State)のバックチェーンを発行者以外のノードに渡す処理を追加
-
IOUIssueFlow.java:(調査3):
- 1つ前のAddressState(=Ref.State)をトランザクションに含める処理を追加
- ※消費済みのRef.Stateは1つ前のRef.Stateとしています。
CDL
AddressStateに関するCDL
重要な制約のみ説明したいと思います。MoveのTLCですが、addressとaddress(new)は変わっていなければならないという制約があります。これはMoveしたのに住所が変更されていないという状況を防ぐためです。
IOUIssueに関するCDL
こちらはIOUのissueにAddressStateが付属した形になります。IOUの発行者の住所録以外が付属されるという状況を防ぐため、IOUの発行者(borrower)とAddressStateの発行者(issuer)は等しくなければならないという制約を加えています。
コード解説
※重要な箇所のみをピックアップして解説したいと思います。
AddressState.java
private static int ID_AddressState=1;
@NotNull
private final Party issuer;
@NotNull
private final String address;
@NotNull
private final UniqueIdentifier linearId;
PartyはAddressStateの発行者、Addressが住所、そしてLinearIdを宣言しました。ID_AddressStateというフィールドですがIOUContract.javaで使用するので、ひとまずここではID_AddressStateという1がハードコーディングされたフィールドがあるという認識で大丈夫です。
AddressContract.java
//1. About InputState
require.using("No Address InputState should be consumed when publishing Address State",
tx.inputsOfType(AddressState.class).isEmpty());
//2. About OutputState
final List<AddressState> outList=tx.outputsOfType(AddressState.class);
require.using("Only one OutputState should be created",
outList.size()==1);
AddressContract.javaにはPublishとMoveの定義・制約が記述されています。まずPublishに関する制約を説明します。PublishなのでInputStateは0個、OutputStateは1個存在しなければいけないという制約を加えました。また、ここには記載されていませんが AddressStateの発行者(Publishした人)の署名がないと話にならないので追加されています。
List<AddressState> inputState=tx.inputsOfType(AddressState.class);
List<AddressState> outputState=tx.outputsOfType(AddressState.class);
//add constraints regarding Move command.
//1. About InputState and OutputState
require.using("Move transaction should only consume and create one InputState and One OutputState.",
inputState.size()==1 &&
outputState.size()==1);
//2. About Address information
require.using("Address information should be changed in Move transaction",
!(inputState.get(0).getAddress()).equals(outputState.get(0).getAddress()));
//3. Other fields should not be changed.
require.using("Only address field should be changed in Move transaction",
inputState.get(0).getIssuer().equals(outputState.get(0).getIssuer()) &&
inputState.get(0).getLinearId().equals(outputState.get(0).getLinearId()));
続いて、Moveに関する制約を説明します。MoveなのでInputStateとOutputStateは1つずつ存在しなければならないという制約を加えました。また、Moveの際にAddressStateのaddressというフィールドは変更されていなければなりません。 引っ越しをしたのに住所が変更されないケースはないと思います。一方、issuerとlinearIdは不変でなければいけません。 それらがMove特有の制約になります。Publish同様、署名に関する制約は記述されています。
PublishFlow.java
//2. Add outputState and Command into TX.
AddressState state=new AddressState(getServiceHub().getMyInfo().getLegalIdentities().get(0), address);
Command txCommand=new Command(new
AddressContract.Commands.Publish(),getServiceHub().getMyInfo().getLegalIdentities().get(0).getOwningKey());
TransactionBuilder txBuilder=new TransactionBuilder(notary)
.addOutputState(state, AddressContract.ADDRESS_CONTRACT_ID)
.addCommand(txCommand);
PublishFlow.javaではPublishを制御するFlowが記述されています。基本的な流れは通常のFlowと同じですが、AddressStateの発行者とNotary間のみでやり取りをしている点に注意してください。
MoveFlow.java
//2. Find already published AddressState.
StateAndRef<AddressState> oldState=getServiceHub().getVaultService().queryBy(AddressState.class).getStates().get(0);
AddressState oldStateData=oldState.getState().getData();
String newAddress=address;
AddressState newAddressState=new AddressState(
oldStateData.getIssuer(),
newAddress,
oldStateData.getLinearId()
);
MoveFlow固有の処理としてInputStateとして用いるAddressStateを探す必要があります。ここではVaultQueryを用いて検索しています。この処理を行うことで、issuerとLinearIdを新たに設定する必要がなくなります。issuerとLinearIdは変わらないのでセットする手間が省けました。
IOUContract.java
private int ID_Contract=1;
ID_Contractというフィールドは、AddressStateで宣言されたID_AddressStateというフィールドと共にAddressStateの制約の一つとして使用されます。以下にAddressStateに関する制約を記載します。
//whether matches IOU lender and AddressState issuer.
AddressState addressState =tx.referenceInputRefsOfType(AddressState.class).get(0).getState().getData();
require.using("The lender of IOUState and the issuer of AddressState should be matched.",addressState.getIssuer().equals(outputState.getBorrower()));
//ID constraints
require.using("ID_AddressState and ID_Contract must be same.",ID_Contract==AddressState.getID_AddressState());
まず一つ目は、AddressStateのissuerとIOUのborrowerが等しくなければならないという制約を加えます。IOUを発行するのはborrowerになります。したがって、borrowerの住所がAddressStateに記載される必要があります。次に、ID_AddressStateとID_Contractが等しいか確認する制約を加えました。この制約が必要な理由として、要望1のRef.Stateをトランザクションに含め、コントラクトで検証か を確かめるためです。共に1がハードコーディングされているので少々強引ですが、これで要望1を満たすことができます。
IOUIssueFlow.java(要望1,2)
public StateAndRef<AddressState> getAddressIssuer(Party addressStateIssuer){
Predicate<StateAndRef<AddressState>> byIssuer=addressISU
->(addressISU.getState().getData().getIssuer().equals(addressStateIssuer));
List<StateAndRef<AddressState>> addressLists = getServiceHub().getVaultService().queryBy(AddressState.class)
.getStates().stream().filter(byIssuer).collect(Collectors.toList());
if(addressLists.isEmpty()){
return null;
}else{
return addressLists.get(0);
}
}
上記は、要望1,2の場合のIOUIssueFlow.java固有のgetAddressIssuer関数になります。トランザクションにAddressStateを含める過程で、含めるAddressStateを選択する重要な処理になります。 具体的には引数のaddressStateIssuerを元に、このissuerのAddressStateが本当に存在するか確認します。存在する場合はその情報をリストに格納してreturnしています。
IOUIssueFlow.java(要望3)
//1. get latest AddressState's hash by using vaultQuery.
QueryCriteria queryCriteria=new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL);
Vault.Page results =getServiceHub().getVaultService().queryBy(AddressState.class,queryCriteria);
//The index is set to 1 to see the item of "ref".
StateAndRef included_addressState=(StateAndRef) results.getStates().get(1);
SecureHash vault_addressHash=included_addressState.getRef().getTxhash();
//2. get previous AddressState's hash by using validatedTransaction.
StateRef results1 =getServiceHub().getValidatedTransactions().getTransaction(vault_addressHash).getInputs().get(0);
SecureHash previousHash=results1.getTxhash();
//Output previous hash to confirm.
System.out.println("previousHash="+previousHash);
上記は、1つ前のAddressStateを探す処理になります。具体的にはまず最新のAddressStateを検索し、そこから1つ前のAddressStateにさかのぼる重要な処理になります。VaultQueryを使い最新のAddressStateのハッシュ値を取り出します。続いて、ValidatedTransactionでそのAddressStateの詳細な情報を調べます。そこからInputとして使われたAddressStateを見つけます。ここで注意していただきたい点は、Inputとして使われてたAddressStateは1つ前のAddressStateであるという点です。 そしてハッシュ値を取り出せばAddressStateのトレースバックは完了です。
public StateAndRef<AddressState> getAddressContent(SecureHash previousHash){
QueryCriteria queryCriteria=new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.ALL);
//search the hash equal to the previous hash and put it in the list.
Predicate<StateAndRef<AddressState>> byHash=address_hash
->(address_hash.getRef().getTxhash().equals(previousHash));
List<StateAndRef<AddressState>> addressHashLists=getServiceHub().getVaultService().queryBy(AddressState.class,queryCriteria)
.getStates().stream().filter(byHash).collect(Collectors.toList());
//Output previous hash to confirm.
System.out.println("the content of list="+ addressHashLists);
if(addressHashLists.isEmpty()){
return null;
}else{
return addressHashLists.get(0);
}
}
続いて前の処理で検索した1つ前のAddressStateのハッシュ値を元に、そのAddressStateの情報を取り出すgetAddressContent()という関数が必要になります。大まかな流れは要望1,2のgetAddressIssuer()と同じになります。異なる点として、issuerではなくハッシュ値で目的のAddressStateが存在するか確認しています。
サンプルプログラムのURL
調査1,2のサンプルプログラムです。
https://github.com/ShotaIMO/investigation-of-ReferenceState
調査3のサンプルプログラムです。
https://github.com/ShotaIMO/investigation-of-Previous-ReferenceState
サンプルプログラム作成にあたって、ご協力いただいたSBI R3 Japan株式会社のエンジニアチームの皆様
ありがとうございました。
終わりに
以上がコード解説編になります。サンプルプログラムを実際に動かした結果は、 デモンストレーション編 で解説したいと思います。続きが気になる方は是非ご一読ください。ここまで読んでいただき、ありがとうございました。