ScalarDL の適切な Contract を作成する方法に関するガイド
このページは英語版のページが機械翻訳されたものです。英語版との間に矛盾または不一致がある場合は、英語版を正としてください。
このドキュメントでは、ScalarDL の Contract を作成するためのガイドラインをいくつか示します。
ScalarDL の Contract とは何ですか?
ScalarDL の Contract (別名スマー トコントラクト) は、単一のビジネスロジックを実装するために記述された、事前定義済みの基本 Contract を拡張する Java プログラムです。Contract とその引数は、Contract 所有者の秘密鍵で電子署名され、ScalarDL に渡されます。この仕組みにより、Contract は所有者のみが実行できるようになり、データ改ざんなどの悪意のある行為をシステムが検知できるようになります。
このドキュメントを参照する前に、ScalarDL 入門を参照して、ScalarDL とは何か、およびその基本用語を理解してください。
簡単な Contract を書く
Contract の書き方をよりよく理解するために、StateUpdater Contract の例を詳しく見てみましょう。
public class StateUpdater extends JacksonBasedContract {
@Nullable
@Override
public JsonNode invoke(Ledger<JsonNode> ledger, JsonNode argument, @Nullable JsonNode properties) {
if (!argument.has("asset_id") || !argument.has("state")) {
// ContractContextException is the only throwable exception in a Contract and
// it should be thrown when a Contract faces some non-recoverable error
throw new ContractContextException("please set asset_id and state in the argument");
}
String assetId = argument.get("asset_id").asText();
int state = argument.get("state").asInt();
Optional<Asset<JsonNode>> asset = ledger.get(assetId);
if (!asset.isPresent() || asset.get().data().get("state").asInt() != state) {
ledger.put(assetId, getObjectMapper().createObjectNode().put("state", state));
}
return null;
}
}
基本 Contract
Ledger データと Contract 引数の内部表現は String です。ただし、String を使用した構造化データの処理はエラーが発生しやすく、必ずしも簡単であるとは限りません。基本 Contract では、Ledger データと Contract 引数の他の扱いやすいデータ型を定義します。また、データ型と文字列間のシリアル化と逆シリアル化も管理します。
たとえば、上記の StateUpdater Contract は、JacksonBasedContract と呼ばれる基本 Contract の1つに基づいています。これにより、Jackson の JsonNode形式で Ledger データと Contract 引数を処理できるようになります。
これを書いている時点では、以下に示す4つの基本 Contract を提供しています。ただし、開発の生産性とパフォーマンスのバランスを適切に保つには、 JacksonBasedContract を使用することをお勧めします。
| 基本 Contract クラス | Contract 引数のタイプ、Contract プロパティ、Contract 出力、およびLedger データ | ライブラリ |
|---|---|---|
| JacksonBasedContract (おすすめ) | JsonNode | Jackson |
| JsonpBasedContract | JsonObject | JSONP |
| StringBasedContract | String | Java標準ライブラリ |
| Contract (廃止された) | JsonObject | JSONP |
古い Contract はまだ利用可能ですが、現在は非推奨となっており、今後のメジャーバージョンで削除される予定です。したがって、上記の新しい (非推奨ではない) Contract を基本 Contract として使用することを強くお勧めします。
invoke 引数について
上に示したように、オーバーライドされた invoke メソッドは、基礎となるデータベースと対話するための Ledger、Contract 引数の JsonNode、およびContract プロパティ用のオプションである JsonNode を受け取ります。
Ledger は一連のアセットを管理するデータベース抽象化であり、各アセットは asset_id と呼ばれるキーと age と呼ばれる履歴バージョン番号によって識別されるレコードの履歴で構成されます。get、put、および scan API を使用して Ledger と対話できます。get API は、指定されたア セットの最新のアセットレコードを取得するために使用されます。put API は、指定されたアセットに新しいアセットレコードを追加するために使用されます。scan API は、指定されたアセットを走査するために使用されます。この抽象化では Ledger にアセットレコードを追加できるだけであることに注意してください。したがって、ScalarDL の Contract を作成する前に、抽象化を使用してデータを設計することを常に推奨します。
Contract 引数は、リクエスタによって指定された Contract の実行時引数です。Contract 引数は通常、ランタイム変数を定義するために使用されます。たとえば、銀行アプリケーションでは、支払い Contract が実行されるたびに、支払者と受取人が引数として Contract に渡される場合があります。
Contract のプロパティは、Contract の静的変数です。これは、Contract のインスタンスごとの静的変数を定義するために使用できます。たとえば、同意管理アプリケーションでは、同意のためのビジネスロジックは一般的な Contract として定義できますが、同意条件は実際のアプリケーションによって異なる場合があります。オプションのプロパティフィールドを使用すると、Contract 内でハードコーディングせずに、各 Contract インスタンスのクォーラムなどの Contract 条件を定義できます。
StateUpdater ロジックについて
StateUpdater Contract は、最初に引数に適切な変数があるかどうか、およびアプリケーションコンテキストと一致しているかどうかをチェックし、適切に定義されていない場合は ContractContextException をスローします。ContractContextException は Contract からスロー可能な唯一の例外であり、要件が完全に満たされていないために Contract の実行を再試行しないようシステムに通知するために使用されます。
次に、Contract はリクエスタから指定された asset_id と state を取得し、指定された asset_id を持つ Ledger から asset を取得します。また、アセットが存在しない場合、またはアセットの状態が現在の状態と異なる場合は、アセットの状態を更新します。
Ledger と対話するときに Contract は RuntimeException に直面する可能性がありますが、Contract 内でそれを捕らえるべきではありません。すべての例外は、ScalarDL エグゼキューターによって適切に処理されます。
この Contract は、指定されたアセットの状態を作成または更新するだけなので、リクエスタに何も返す必要はありません。したがって、この場合は null を返すことができます。リクエスタに何かを返したい、かつ JacksonBasedContract を使用する場合は、任意の JsonNode を返すことができます。
アセットのグループ化
asset_id の値は任意に定義できますが、アセットをグループ化する場合は、いくつかのルールを設けることをお勧めします。
たとえば、特定の世代にグループ化したい場合は、{asset_id}-0 のようにアセットに世代番号を追加します。
または、{org-id}-{asset_id}のようなプレフィックスとして組織 ID を付けることで、組織ごとにグループ化することもできます。
例外処理
上で述べたように ContractContextException をスローする場合を除いて、Contract 内で例外処理を行うべきではないことに注意してください。
したがって、 Ledger は、何らかの理由で続行できない場合に実行時 (チェックされていない) 例外をスローする可能性がありますが、例外はキャッチされるべきではありません。例外は Contract 外で適切に処理されます。
決定論
ScalarDL の Contract を作成するときに注意すべき非常に重要なことの1つは、Contract を決定論的にする必要があるということです。言い換えれば、Contract は、特定の入力に対して常に同じ出力を生成する必要があります。これは、ScalarDL が決定論を利用して改ざんを検知するためです。
たとえば、ScalarDL はアセットを遅延的に走査し、Contract を再実行して、期待される結果と Ledger に保存されている実際のデータとの間に矛盾がないかどうかを確認します。また、決定論を利用して、複数の独立した ScalarDL コンポーネント (つまり、Ledger と Auditor) の状態を同じにします。
非決定的な Contract を作成する一般的な方法の1つは、Contract 内で時間を生成し、Ledger の状態を含む出力が何らかの形でこの時間に依存するようにすることです。このような Contract は実行されるたびに異なる出力を生成するため、システムは改ざんを検知できなくなります。Contract で時間を使用する必要がある場合は、それを引数として Contract に渡す必要があります。
アセットの削除
Contract を通じて登録されたアセットは、改ざんの証拠を提供するために削除することができません。ただし、開発するアプリケーションのルールや規制に従うために、一部のアセットを削除したい場合があります。このようなデータ削除を提供するために、ScalarDL は Function と呼ばれる機能をサポートしています。
Function の詳細については、ScalarDL Function の書き方ガイドを参照してください。
Function に情報を送信する
JacksonBasedContract のような非推奨ではない Contract では、void setContext(T context) を呼 び出すことで Function に情報を送信できます。
使用する基本 Contract クラスによって引数の型 T が決定されることに注意してください。
Function で Contract から情報を受け取る方法の詳細については、Contract から情報を受け取るを参照してください。
JsonNode context = getObjectMapper().createObjectNode().put(...);
setContext(context);
複雑な Contract を作成する
このセクションでは、複雑な Contract の書き方について説明します。
ネストされた方法で Contract を呼び出す
Contract のコードが100行を超える場合は、その Contract で複数のことを実行している可能性があることを示す良い兆候です。 各 Contract が1つのことだけを実行するモジュール化された Contract を作成し、Contract を結合してより複雑なビジネスロジックを表現することは良い習慣です。
以下は、そのようなネストされた呼び出しを実行するコード例です。指定されたアセットの状態を読み取る StateReader が state-reader という Contract ID で登録されているものとします。
public class StateUpdaterReader extends JacksonBasedContract {
@Nullable
@Override
public JsonNode invoke(
Ledger<JsonNode> ledger, JsonNode argument, @Nullable JsonNode properties) {
if (!argument.has("asset_id") || !argument.has("state")) {
// ContractContextException is the only throwable exception in a Contract and
// it should be thrown when a Contract faces some non-recoverable error
throw new ContractContextException("please set asset_id and state in the argument");
}
String assetId = argument.get("asset_id").asText();
int state = argument.get("state").asInt();
Optional<Asset<JsonNode>> asset = ledger.get(assetId);
if (!asset.isPresent() || asset.get().data().get("state").asInt() != state) {
ledger.put(assetId, getObjectMapper().createObjectNode().put("state", state));
}
return invoke("state-reader", ledger, argument);
}
}
StateUpdaterReader は、StateUpdater と同じように Ledger を更新し、さらに別の invoke で state-reader 呼び出して、書き込まれた 内容を読み取ります。この例はあまり説得力がないかもしれませんが、Contract をモジュール化する (たとえば、StateUpdater を個別に定義する) と、Contract を再利用可能にすることができます。
ネストされた呼び出し内のすべての Contract は、ScalarDL でトランザクション的に (ACID 方式で) 実行されるため、完全に成功するか完全に失敗するかのどちらかになる点に注意してください。
誰がアセットにアクセスできるかを管理する
getClientIdentityKey() を呼び出すと、Contract 内で Contract を実行しているユーザーを示す ID 情報を取得できます。この機能は、特定のアセットにアクセスできるユーザーを制御するのに役立ちます。次の例は、state-xxx (xxx は証明書またはシークレットのホルダーのエンティティ ID) という名前の制限付きアセットのみを更新できるように変更された StateUpdater を示しています。
詳細については、Javadoc の基本 Contractセクションと ClientIdentityKey ページを参照してください。
public class StateUpdater extends JacksonBasedContract {
@Nullable
@Override
public JsonNode invoke(Ledger<JsonNode> ledger, JsonNode argument, @Nullable JsonNode properties) {
if (!argument.has("state")) {
throw new ContractContextException("please set state in the argument");
}
ClientIdentityKey clientIdentityKey = getClientIdentityKey();
String entityId = clientIdentityKey.getEntityId();
String assetId = "state-" + entityId;
int state = argument.get("state").asInt();
Optional<Asset<JsonNode>> asset = ledger.get(assetId);
if (!asset.isPresent() || asset.get().data().get("state").asInt() != state) {
ledger.put(assetId, getObjectMapper().createObjectNode().put("state", state));
}
return null;
}
}
名前空間でアセットを管理する
名前空間機能は現在パブリックプレビュー中です。この機能と関連ドキュメントは変更される可能性があります。
デフォルト では、Contract は事前設定されたデフォルト名前空間でアセットを読み書きしますが、他の名前空間を作成して各名前空間でアセットを管理することができます。名前空間の作成方法の詳細については、ScalarDL クライアントコマンドリファレンスを参照してください。
以下は、名前空間を認識する StateUpdaterReader Contract の例を示しています。get と put API では、名前空間を指定することで特定の名前空間にアクセスできます。scan API を使用する場合は、AssetFilter クラスで名前空間を指定できます。
public class NamespaceAwareStateUpdaterReader extends JacksonBasedContract {
@Nullable
@Override
public JsonNode invoke(
Ledger<JsonNode> ledger, JsonNode argument, @Nullable JsonNode properties) {
if (!argument.has("namespace") || !argument.has("asset_id") || !argument.has("state")) {
throw new ContractContextException("please set namespace, asset_id, and state in the argument");
}
String namespace = argument.get("namespace").asText();
String assetId = argument.get("asset_id").asText();
int state = argument.get("state").asInt();
Optional<Asset<JsonNode>> asset = ledger.get(namespace, assetId);
if (!asset.isPresent() || asset.get().data().get("state").asInt() != state) {
ledger.put(namespace, assetId, getObjectMapper().createObjectNode().put("state", state));
}
return invoke("state-reader", ledger, argument);
}
}