Token SDKの実装について⑤(”進化可能”なTokenの作成を改善する)

はじめに

本稿について

本稿は、CordaのToken開発キットである「Token SDK」を使用してオリジナルの"進化可能"なTokenの改善について考えていきます。

また、本稿はR3が提供しているToken SDKのトレーニング「Make a car sale」と「Car Sale Solution」に沿った内容になっています。

本稿を読み進める前に、前回記事をご覧いただくことをお勧めします。

 

改善の方針

現在、現状Car Tokenは買い手から売り手に単純に移転「のみ」されるような仕組みになっています。

mceclip0.png

ただ、実際の売買は相互に物の相互交換が発生するようなケースもあります。具体的には、AliceがCar Tokenを渡すのと同時に、BobからはTokenを渡すことで売買取引が実現します。

mceclip1.png

上記のTokenの移転は同じTransactionに含まれているため、アトミックです。すなわち両方とも成功するか両方とも失敗するかどちらかになります。

上記のような売買flowを実装する方法を考えてみます。

  • 一方の当事者であるイニシエーターがTransactionを作成し、もう一方の当事者であるレスポンダーがそれを検証して署名する必要があります。開始側が買い手なのか、売り手なのかは業務要件に応じて、自由に設定できます。今回は売り手がイニシエーター、買い手がレスポンダーとします。
  • 売り手は、車のStateAndRef<NonFungibleToken>を提供する必要があります。
  • 買い手は、提供されたNonFungibleTokenが本当に希望の車を表しているかどうかを確認しなければなりません。売り手が全く違った車を提供してくる恐れがあるためです。
  • 買い手は、車の代金に見合った通貨のStateAndRef<FungibleToken>を提供しなければなりません。
  • 翻って売り手は、買い手が提示した代金が適切な価格であるか検証する必要があります。
  • レスポンダーは、署名を求められたトランザクションが、期待したトランザクションであることを確認しなければならない。イニシエーター全く違ったTransactionを送信してきて署名を要求する恐れがあるためです。

 

実装

イニシエーターflow(CarSeller)の処理①

売り手のパラメーターを以下のように定義します。

@NotNull
private final TokenPointer<CarTokenType> car;
@NotNull
private final Party buyer;
@NotNull
private final IssuedTokenType issuedCurrency;

まずは、最新のcar Stateを取得します。

final StateAndRef<CarTokenType> carInfo = car.getPointer().resolve(getServiceHub());

買い手が確認できるように、取得したcar情報を「SendStateAndRefFlow」で渡します。

final FlowSession buyerSession = initiateFlow(buyer);
subFlow(new SendStateAndRefFlow(buyerSession, Collections.singletonList(carInfo)));

SendStateAndRefFlow」はStateAndRefを相手Nodeに送信するためのflowです。レスポンダー側は「ReceiveStateAndRefFlow」で受信する必要があります。この「ReceiveStateAndRefFlow」は渡されたStateAndRefが正しいものかを検証する便利なflowです。

 

並行して、carの価格およびcarを保有していることを確認します。ここでは厳密にはcarの情報に該当するNonFungibleTokenを保有情報を確認します。

final long price = carInfo.getState().getData().getPrice();

final QueryCriteria tokenCriteria = heldTokenCriteria(car);
final List<StateAndRef<NonFungibleToken>> heldCarTokens = getServiceHub().getVaultService()
.queryBy(NonFungibleToken.class, tokenCriteria).getStates();
if (heldCarTokens.size() != 1) throw new FlowException("NonFungibleToken not found");

ここで取得した「heldCarTokens 」も「SendStateAndRefFlow」にて相手Nodeに送ります。

subFlow(new SendStateAndRefFlow(buyerSession, heldCarTokens));

併せて、支払うための通貨情報を送ります。

buyerSession.send(issuedCurrency);

ここまでで売り手は買い手が必要な情報をすべて送信しています。買い手から支払い情報などを受信を待たずにTransactionを生成します。ここではいつも通りNotaryを選んでTransactionを生成します。

final Party notary = carInfo.getState().getNotary();
final TransactionBuilder txBuilder = new TransactionBuilder(notary);

Transactionを生成したらTokenを追加します。addMoveNonFungibleTokensはTransactionに適切なinput Stateとoutput Stateを設定します。この場合の適切なinput Stateとoutput Stateは以下の通りになります。

input State: 持ち主 - 売り手

output State: 持ち主 - 買い手

final PartyAndToken carForBuyer = new PartyAndToken(buyer, car);
MoveTokensUtilitiesKt.addMoveNonFungibleTokens(txBuilder, getServiceHub(), carForBuyer, null);

この時点でのTransactionはcarの移転のみが含まれています。USDのToken情報は買い手からの受信する必要があります。「ReceiveStateAndRefFlow」は相手NodeからStateAndRefを受信するためのflowです。

final List<StateAndRef<FungibleToken>> currencyInputs = subFlow(new ReceiveStateAndRefFlow<>(buyerSession));

 

レスポンダーflow(CarBuyer)の処理①

翻って、買い手側の実装について考えます。まず「ReceiveStateAndRefFlow」を行っていますが、これは売り手からのcar情報を受け取るためのものです。もらったcarの情報から価格を取得します。


final List<StateAndRef<CarTokenType>> carInfos = subFlow(new ReceiveStateAndRefFlow<>(sellerSession));
if (carInfos.size() != 1) throw new FlowException("We expected a single car type");
final StateAndRef<CarTokenType> carInfo = carInfos.get(0);
final long price = carInfo.getState().getData().getPrice();

再び「ReceiveStateAndRefFlow」を実行します。これは2番目に売り手から送信された「heldCarTokens」に対応します。car情報とheldCarTokensが等しいものかどうかをチェックします。

final List<StateAndRef<NonFungibleToken>> heldCarTokens = subFlow(new ReceiveStateAndRefFlow<>(sellerSession));
if (heldCarTokens.size() != 1) throw new FlowException("We expected a single held car");
final StateAndRef<NonFungibleToken> heldCarToken = heldCarTokens.get(0);

if (!((TokenPointer<CarTokenType>) heldCarToken.getState().getData().getTokenType())
.getPointer().getPointer()
.equals(carInfo.getState().getData().getLinearId()))
throw new FlowException("The owned car does not correspond to the earlier car info.");

最後に支払いに使うための通貨情報を受け取ります。

final IssuedTokenType issuedCurrency = sellerSession.receive(IssuedTokenType.class).unwrap(it -> it);

支払う通貨を保有しているかをチェックします。持っている場合、USD tokenにまつわるinput Stateとoutput Stateを生成します。これらは「generateMove」を使うと簡単に生成できます。

final QueryCriteria heldByMe = QueryUtilitiesKt.heldTokenAmountCriteria(
issuedCurrency.getTokenType(), getOurIdentity());
final QueryCriteria properlyIssued = QueryUtilitiesKt.tokenAmountWithIssuerCriteria(
issuedCurrency.getTokenType(), issuedCurrency.getIssuer());
final Amount<TokenType> priceInCurrency = AmountUtilitiesKt.amount(price, issuedCurrency.getTokenType());
final DatabaseTokenSelection tokenSelection = new DatabaseTokenSelection(
getServiceHub(), MAX_RETRIES_DEFAULT, RETRY_SLEEP_DEFAULT, RETRY_CAP_DEFAULT, PAGE_SIZE_DEFAULT);
final Pair<List<StateAndRef<FungibleToken>>, List<FungibleToken>> inputsAndOutputs = tokenSelection.generateMove(
Collections.singletonList(new Pair<>(sellerSession.getCounterparty(), priceInCurrency)),
getOurIdentity(),
new TokenQueryBy(issuedCurrency.getIssuer(), it -> true, heldByMe.and(properlyIssued)),
getRunId().getUuid());

生成したinput State(厳密にはStateではなくStateAndRefです)とoutput Stateは売手のTransactionに必要なため、情報を送信します。まずはinput Stateの情報を渡します。

subFlow(new SendStateAndRefFlow(sellerSession, inputsAndOutputs.getFirst()));

即座にoutput Stateの情報を渡します。

sellerSession.send(inputsAndOutputs.getSecond());

ここまでで買い手から売り手に渡すべき情報がすべて渡されました。次に「SignTransactionFlow」を行い、買い手は売り手からの署名要求を受信するまで待ち状態になります。

final SecureHash signedTxId = subFlow(new SignTransactionFlow(sellerSession) {

 

イニシエーターflow(CarSeller)の処理②

売り手側の処理に戻ります。まずは、買い手側から送信されたUSD tokenにまつわるinput Stateとoutput Stateの情報を受信します。また、受信したinput StateのTokenに関する検証も行います。今回の例では、USD tokenに売り手保有のtokenが混ざっていないかを検証しています。

final List<StateAndRef<FungibleToken>> currencyInputs = subFlow(new ReceiveStateAndRefFlow<>(buyerSession));

final long ourCurrencyInputCount = currencyInputs.stream()
.filter(it -> it.getState().getData().getHolder().equals(getOurIdentity()))
.count();
if (ourCurrencyInputCount != 0)
throw new FlowException("The buyer sent us some of our token states: " + ourCurrencyInputCount);

次にUSD Tokenのoutputも受信します。またtokenの正当性についても検証します。今回の場合だと、期待している通貨および金額が期待している値を満たしているかを確認しています。

final List<FungibleToken> currencyOutputs = buyerSession.receive(List.class).unwrap(it -> it);
final long sumPaid = currencyOutputs.stream()
.filter(it -> it.getHolder().equals(getOurIdentity()))
.map(FungibleToken::getAmount)
.filter(it -> it.getToken().equals(issuedCurrency))
.map(Amount::getQuantity)
.reduce(0L, Math::addExact);
if (sumPaid < AmountUtilitiesKt.amount(price, issuedCurrency.getTokenType()).getQuantity())
throw new FlowException("We were paid only " +
sumPaid / AmountUtilitiesKt.amount(1L, issuedCurrency.getTokenType()).getQuantity() +
" instead of the expected " + price);

USD tokenが適切であることが確認できればTransactionにtokenを組み込みます。

MoveTokensUtilitiesKt.addMoveTokens(txBuilder, currencyInputs, currencyOutputs);

Transactionが完成したので、売り手はTransactionに署名します。

final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder,
getOurIdentity().getOwningKey());

次に署名要求を買い手に送信します。

final SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(partSignedTx,
        Collections.singletonList(buyerSession)));

 

レスポンダーflow(CarBuyer)の処理②

買い手は売り手から署名要求を受け取りました。そのまま署名することもできますが、買い手側で特殊なチェックを含めることも可能です。このような追加の実装を行いたい場合は「checkTransaction」の中で行います。まずはTransactionのinput Stateを検証するために自身が生成したUSD input Stateと現状の車情報「heldCarToken」を期待するinput Stateのリストとして抽出します。

final Set<StateRef> allKnownInputs = inputsAndOutputs.getFirst().stream()
        .map(StateAndRef::getRef)
        .collect(Collectors.toSet());
allKnownInputs.add(heldCarToken.getRef());

上記のリストとTransactionのinput Stateに整合性が取れているかを検証します。

final Set<StateRef> allInputs = new HashSet<>(stx.getInputs());
if (!allInputs.equals(allKnownInputs))
        throw new FlowException("Inconsistency in input refs compared to expectation");

次にoutput Stateの検証です。まずは、Transactionのoutput Stateのjサイズが期待しているものかをチェックします。

final List<ContractState> allOutputs = stx.getCoreTransaction().getOutputStates();
if (allOutputs.size() != inputsAndOutputs.getSecond().size() + 1)
throw new FlowException("Wrong count of outputs");

次にTransactionに含まれるUSD Tokenが買い手が送信したoutput Token情報と等しいことや余計なTokenが含まれていないことを確認します。

final List<FungibleToken> allCurrencyOutputs = allOutputs.stream()
.filter(it -> it instanceof FungibleToken)
.map(it -> (FungibleToken) it)
.filter(it -> it.getIssuedTokenType().equals(issuedCurrency))
.collect(Collectors.toList());
if (!inputsAndOutputs.getSecond().equals(allCurrencyOutputs))
throw new FlowException("Inconsistency in FungibleToken outputs compared to expectation");

次にcarについての検証に移ります。まずはTransaction中のNonFungibleTokenの総数を検証します。

final List<NonFungibleToken> allCarOutputs = allOutputs.stream()
        .filter(it -> it instanceof NonFungibleToken)
        .map(it -> (NonFungibleToken) it)
        .collect(Collectors.toList());
if (allCarOutputs.size() != 1) throw new FlowException("Wrong count of car outputs");

Transactionのcarが当初受け取っていたcarと同一のものかをlinear IDをもとにチェックします。併せて所有者が正しく買い手になっていることをチェックします。

final NonFungibleToken outputHeldCar = allCarOutputs.get(0);
if (!outputHeldCar.getLinearId().equals(heldCarToken.getState().getData().getLinearId()))
        throw new FlowException("This is not the car we expected");

if (!outputHeldCar.getHolder().equals(getOurIdentity()))
throw new FlowException("The car is not held by us in output");

Transactionに関することであれば、他にも追加の検証が可能です。例えば、コマンドが期待しているコマンドであることやコマンドの総数が適切であること等です。

final List<Command<?>> commands = stx.getTx().getCommands();
if (commands.size() != 2) throw new FlowException("There are not the 2 expected commands");
final List<?> tokenCommands = commands.stream()
        .map(Command::getValue)
        .filter(it -> it instanceof MoveTokenCommand)
        .collect(Collectors.toList());
if (tokenCommands.size() != 2)
        throw new FlowException("There are not the 2 expected move commands");

全ての検証が完了したら、署名をして売り手に戻します。買い手は「ReceiveFinalityFlow」を実行してファイナライズに備えます。

return subFlow(new ReceiveFinalityFlow(sellerSession, signedTxId));

 

イニシエーターflow(CarSeller)の処理③

署名を受領したら売り手は「FinalityFlow」を実行してNotaryへの署名要求および永続化を行います。

final SignedTransaction notarised = subFlow(new FinalityFlow(
        fullySignedTx, Collections.singletonList(buyerSession)));

最後にこれらの変更あった旨を当事者以外の知るべきNodeに送信します。例えば、Car State中のmaintainerなどが該当します。

subFlow(new UpdateDistributionListFlow(notarised));

flowはSignedTransactionを戻します。

return notarised;

 

おわりに

複数種類のtoken SDKをTransactionに含めた処理を検討してみました。またレスポンダー側でも様々な追加検証を行い、セキュアなCorDappsを構築できました。皆様の開発のご参考になれば幸いです。

 

Created by: Kazuto Tateyama

Last edited by: Kazuto Tateyama

Updated: 2021/06/15

この記事は役に立ちましたか?
0人中0人がこの記事が役に立ったと言っています