Token SDKの実装について④(”進化可能”なTokenの作成)

はじめに

本稿について

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

また、本稿はR3が提供しているToken SDKのトレーニング「Create a new Token」に沿った内容になっています。

 

前提知識

"Evolvable"なTokenとは?

「Evolvable」とは日本語にすると「進化できる」と訳されます。つまり、EvolvableなTokenはそのライフサイクル中においてプロパティの値が変更されうるようなTokenを指します。

基本的な概念に関しては、こちらの記事もご参考ください。

 

実装

State

このような性質のTokenを実装するには、「EvolvableTokenType」を拡張して作ることができます。
EvolvableTokenTypeはLinearStateを継承しているため、ほかのTokenと区別して扱うことができます。また、プロパティの状態が変化してもlinear IDを持っているため、Tokenの変化を追跡できます。

以下は、CarTokenTypeというEvolvableTokenを実装しています。フィールドにUniqueIdentifierが定義されていて、一意のIDを付与させることができます。

@BelongsToContract(CarTokenContract.class)
public class CarTokenType extends EvolvableTokenType {

public static final int FRACTION_DIGITS = 0;
@NotNull
private final List<Party> maintainers;
@NotNull
private final UniqueIdentifier uniqueIdentifier;
@NotNull
private final String VIN;
@NotNull
private final String make;
private final long mileage;
private final long price;



コンストラクタやgetter, setterを定義


トークンを適切にグループ化したり、HashMapで使用するためには、`equals()`と`hashCode`が必要なため忘れずに実装します。

    // We require `equals()` and `hashCode` to properly group Tokens, and also to be able to use
// them in a HashMap.
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final CarTokenType that = (CarTokenType) o;
return getFractionDigits() == that.getFractionDigits() &&
Double.compare(that.getMileage(), getMileage()) == 0 &&
Double.compare(that.getPrice(), getPrice()) == 0 &&
maintainers.equals(that.maintainers) &&
uniqueIdentifier.equals(that.uniqueIdentifier) &&
getVIN().equals(that.getVIN()) &&
getMake().equals(that.getMake());
}

@Override
public int hashCode() {
return Objects.hash(getFractionDigits(), maintainers, uniqueIdentifier, getVIN(), getMake(),
getMileage(), getPrice());
}

 

Contract

上記のCarTokenTypeの一行目に以下のように定義しています。

@BelongsToContract(CarTokenContract.class)

これは「CarTokenTypeはCarTokenContractに紐づけられる」という定義なので、「CarTokenContract」を実装します。ただ、基本的な検証は継承元の「EvolvableTokenContract 」にて実装されています。

(例えばtoken createの場合は、Input Stateがないことや、output Stateのサイズが1であることなどです)

もし追加の検証を行いたい場合は、「additionalCreateChecks」 や「additionalUpdateChecks」をオーバーライドして実装します。

public class CarTokenContract extends EvolvableTokenContract implements Contract {

// additionalCreateChecksはEvolvableTokenContractに宣言されている抽象メソッドです。
// ここに、Token生成時に必要な検証ロジックを記載します。
@Override
public void additionalCreateChecks(@NotNull final LedgerTransaction tx) {
final CarTokenType outputCarTokenType = tx.outputsOfType(CarTokenType.class).get(0);
requireThat(require -> {
// Validation rules on our fields.
require.using("Mileage must start at 0.",
outputCarTokenType.getMileage() == 0L);
require.using("Price cannot be 0.",
outputCarTokenType.getPrice() > 0L);
return null;
});
}

// additionalUpdateChecksはEvolvableTokenContractに宣言されている抽象メソッドです。
// ここに、Token更新時に必要な検証ロジックを記載します。
@Override
public void additionalUpdateChecks(@NotNull final LedgerTransaction tx) {
final CarTokenType inputCarTokenType = tx.inputsOfType(CarTokenType.class).get(0);
final CarTokenType outputCarTokenType = tx.outputsOfType(CarTokenType.class).get(0);
requireThat(require -> {
// Validation rules on our fields.
require.using("VIN cannot be updated.",
outputCarTokenType.getVIN().equals(inputCarTokenType.getVIN()));
require.using("Make cannot be updated.",
outputCarTokenType.getMake().equals(inputCarTokenType.getMake()));
require.using("Mileage cannot be decreased.",
outputCarTokenType.getMileage() >= inputCarTokenType.getMileage());
require.using("Price cannot be 0.",
outputCarTokenType.getPrice() > 0L);
return null;
});
}
}

 

Flow

  1. Tokenを生成します。Tokenの生成には「CreateEvolvableTokens」を使うことができます。CreateEvolvableTokensの第一パラメータにToken, 第二パラメータには、Observer Nodeを渡します。(Observer Nodeへの伝播が不要の場合は、空のListを渡します。)今回のケースでは、DMV(自動車管理局)がDealerに車の情報をTokenとして発行しています。
    // Department of Motor Vehicles is the maintainer.
    final Party dmv = getServiceHub().getNetworkMapCache().getPeerByLegalName(
    CordaX500Name.parse("O=DMV, L=New York, C=US"));
    final CarTokenType bmw = new CarTokenType(Collections.singletonList(dmv),
    new UniqueIdentifier(), "abc123", "BMW", 0L, 30_000L);
    final TransactionState bmwTxState = new TransactionState(bmw, notary);
    // Identify the future issuer.
    final Party bmwDealer = getServiceHub().getNetworkMapCache().getPeerByLegalName(
    CordaX500Name.parse("O=BMW Dealership, L=New York, C=US"));
    final SignedTransaction bmwTx = subFlow(new CreateEvolvableTokens(
    bmwTxState, Collections.singletonList(bmwDealer));
    final StateAndRef<CarTokenType> bmwStateAndRef = bmwTx
    .outRefsOfType(CarTokenType.class).get(0);
  2. Tokenが生成されて、Dealerに伝播されました。Dealerは受け取った後、それをもとにNon-fungibleTokenを生成してAliceに発行します。
    final TokenPointer<CarTokenType> bmwPointer = bmw.toPointer(CarTokenType.class);
    // Identify the issuer.
    final Party bmwDealer = getServiceHub().getNetworkMapCache().getPeerByLegalName(
    CordaX500Name.parse("O=BMW Dealership, L=New York, C=US"));
    final IssuedTokenType bmwWithDealership = new IssuedTokenType(bmwDealer, bmwPointer);
    // Create a unique car.
    final NonFungibleToken alicesCar = new NonFungibleToken(bmwWithDealership, alice, new UniqueIdentifier(), null);
    // Issue car to Alice.
    subFlow(new IssueTokens(Collections.singletonList(alicesCar)));
  3. 車が発行されてしばらくしてから、AliceはBobに車を売ろうと考えました。Aliceは、保有している車の情報を更新する必要があります。今回は、走行距離と価格を修正してTokenを更新します。Tokenの更新には、「UpdateEvolvableToken」を使うことができます。
    // Update the car mileage and price.
    final CarTokenType updatedBmw = new CarTokenType(bmw.getMaintainers(), bmw.getLinearId(),
    bmw.getVIN(), bmw.getMake(), 8_000L, 22_000L);
    final SignedTransaction updatedCarTokenTypeStateAndRef = subFlow(new UpdateEvolvableToken(
    bmwStateAndRef, updatedBmw, Collections.singletonList(alice)));
  4. 更新したTokenをBobに売ります。これはCorda上はTokenを移転させるだけです。
    // Sell the car to Bob.
    final TokenPointer<CarTokenType> updatedBmwPointer = updatedBmw.toPointer(CarTokenType.class);
    final PartyAndToken bobAndBmw = new PartyAndToken(bob, updatedBmwPointer);
    subFlow(new MoveNonFungibleTokens(bobAndBmw, Collections.emptyList()));

 

Hint

  • Observer Nodeとは、Stateの参加者ではないがネットワークで起こった内容を把握する当事者のことを指します。(監査人や規制当局など)例えば今回だと、DMVがTokenを発行したタイミングで、Dealerに通知しています。その情報をもとにDealerはAliceに車を発行しています。
  • EvolvableTokenとそれに関連するNon fungible Tokenは独立しているため、それら一方が当事者に通知されないリスクがあります。そのリスクを解消するために「UpdateDistributionListFlow」を使うことでTransaction内の当事者にもれなく通知することができます。
    subFlow(new UpdateDistributionListFlow(fullySignedTx));
  • TokenType または EvolvableTokenType を拡張する場合は、equals() メソッドをオーバーライドする必要があります。そうしないと、「There is a token group with no assigned command」という例外が発生します。これは、SDKがトークン(トランザクションの入出力など)をIssuedTokenTypeでグループ化し、各トークングループに正しいコマンドを割り当てようとするためで、これにより正しい検証Contractを実行することができます。

おわりに

基本的なEvolvable Tokenについて学習しました。次回は、上記のCorDappsをさらに改善します。

 

Created by: Kazuto Tateyama

Last edited by: Kazuto Tateyama

Updated: 2021/05/12

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