これは速習Symbol(SDK v3版)の学習メモです。 ChatGPTやGeminiを用いて学習しました。
目次
- 事前準備
- 第2章 環境構築
- 第3章 アカウント
- 第4章 トランザクション
- 第5章 モザイク
- 第6章 ネームスペース
- 7.メタデータ
- 8.ロック
- 9.マルチシグ化
- 10.監視
- 11.制限
- 12.オフライン署名
- 第13章 検証
事前準備
ノード一覧より任意のノードのページを Chrome ブラウザなどで開きます。本書ではテストネットを前提として解説しています。
任意のノードからHeightを見て、httpsをクリックします
すると以下の画面になり、そしたら開発者コンソールを開きます。

第2章 環境構築
サンプルソースコード
Symbol SDK v3 の読み込み
const SDK_VERSION = "3.3.0";
const sdk = await import(
`https://www.unpkg.com/symbol-sdk@${SDK_VERSION}/dist/bundle.web.js`
);
const sdkCore = sdk.core;
const sdkSymbol = sdk.symbol;
// Buffer を読み込んでおく
const script = document.createElement('script');
script.src = 'https://bundle.run/buffer@6.0.3';
document.head.appendChild(script);
このコードは、ブラウザ環境で Symbol SDK(v3.3.0)を動かすための準備処理です。 Node.js 前提の SDK を CDN 経由で読み込み、ブラウザ不足分(Buffer)を補う、という意味を持っています。 このとき、このコードコンソール画面入力した時エラーが出たときはコピペ禁止になっている設定になっているかもしれないので、allow pasting を直接打ってから、先ほどのコードを入力すれば良いです。
引用:https://qiita.com/flano_yuki/items/adc778aa8bcede2c10e9
① 使用する Symbol SDK のバージョン指定
const SDK_VERSION = "3.3.0";
- 使用する SDK の 固定バージョン
- 実装の再現性・互換性確保のため重要
- 将来の breaking change を避ける
② unpkg から SDK を動的 import
const sdk = await import(
`https://www.unpkg.com/symbol-sdk@${SDK_VERSION}/dist/bundle.web.js`
);
何をしている?
npm パッケージ symbol-sdkの ブラウザ用バンドルをCDN(unpkg)から直接ロード
👉ビルド環境不要・HTML + JS だけで動く
const sdkCore = sdk.core;
const sdkSymbol = sdk.symbol;
意味
- sdk.core 暗号・鍵・ハッシュなど 共通基盤
- sdk.symbol Symbol 固有(Tx, Mosaic, Account など)
👉SDK内部の構造をそのまま使いやすくしている
④ Buffer をブラウザに追加
(script = document.createElement('script')).src =
'https://bundle.run/buffer@6.0.3';
document.getElementsByTagName('head')[0].appendChild(script);
なぜ必要?
- Symbol SDK は内部で Buffer を使用
- しかしブラウザには Node.js の Buffer が存在しない
- → polyfill が必要
何をしている?
- buffer@6.0.3 を CDN から読み込み
- <script> として <head> に追加
- 結果:
window.Buffer
が使えるようになる
共通するロジック
const NODE = window.origin; // 現在開いているページのURL
// fetch /node/info
let networkType, generationHash;
fetch(new URL('/node/info', NODE), {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
.then(json => {
networkType = json.networkIdentifier;
generationHash = json.networkGenerationHashSeed;
});
// fetch /network/properties
let epochAdjustment, identifier, facade;
fetch(new URL('/network/properties', NODE), {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
.then(res => res.json())
.then(json => {
const e = json.network.epochAdjustment;
epochAdjustment = Number(e);
identifier = json.network.identifier; // v3 only
facade = new sdkSymbol.SymbolFacade(identifier); // v3 only
});
// clog関数
function clog(signedTx){
if (!facade) {
console.error("facade is not initialized yet.");
return;
}
let hash = facade.hashTransaction(signedTx).toString();
console.log(NODE + "/transactionStatus/" + hash);
console.log(NODE + "/transactions/confirmed/" + hash);
console.log("https://symbol.fyi/transactions/" + hash);
console.log("https://testnet.symbol.fyi/transactions/" + hash);
}
このコードは、今アクセスしている Symbol ノードの情報を取得して SDK を初期化し、 送信したトランザクションを追跡しやすくするための補助関数を用意する処理です。
要するに「このページ=ノード」として Symbol を動かす準備+Tx確認用URL生成」をやっています。
① 現在のページURLをノードとして使う
const NODE = window.origin;
- このページ自身が Symbol ノード
② Buffer をグローバルで使えるようにする
const Buffer = buffer.Buffer;
- 先に読み込んだ buffer@6.0.3 の polyfill
- Symbol SDK 内部で使用される
③ ノード情報を取得
fetch('/node/info')
取得している情報
networkType = json.networkIdentifier;
generationHash = json.networkGenerationHashSeed;

これは速習Symbol(SDK v3版)の学習メモです。 ChatGPTやGeminiを用いて学習しました。これは速習Symbol(SDK v3版)の学習メモです。 ChatGPTやGeminiを用いて学習しました。
これを間違えると 署名が無効になる
④ ネットワーク設定を取得
fetch('/network/properties')
取得後の処理
e = json.network.epochAdjustment;
epochAdjustment = Number(e.substring(0, e.length - 1));
identifier = json.network.identifier;
facade = new sdkSymbol.SymbolFacade(identifier);
各項目の意味
⏰ epochAdjustment
- Symbol 独自の「ネットワーク時刻補正」
- Deadline 計算に使用
🌐 identifier(v3 SDK)
- ネットワーク識別子
- SymbolFacade 初期化に必須
🧱 SymbolFacade
- Symbol SDK v3 の中核オブジェクト
- Tx作成・署名・ハッシュ計算を担当
⑤ トランザクション追跡用の補助関数
function clog(signedTx){
何をする関数?
- 署名済みトランザクションから
- Txハッシュを計算
- 確認用URLをまとめて表示
出力されるURL
NODE + "/transactionStatus/" + hash
NODE + "/transactions/confirmed/" + hash
https://symbol.fyi/transactions/{hash}
https://testnet.symbol.fyi/transactions/{hash}
意味
- ノード直アクセス(JSON)
- ブロックエクスプローラ(GUI)
👉 デバッグ・学習用途に非常に便利
fetch(フェッチ) 関数について
fetch(フェッチ) とは、JavaScript が持つ HTTP 通信を行うための関数 です。ブラウザや Node.js で URL にアクセスしてデータを取ってくる(GET) ことも、データを送る(POST) こともできます。まとめると、以下にななります
- ブラウザからインターネットにアクセスするための関数
- サーバーにリクエストを送り、レスポンス(データ)を受け取る
- JSON API を呼ぶときに必須の関数
基本形
fetch("https://example.com/api")
.then(response => response.json())
.then(data => console.log(data));
やっていること:
- fetch() で指定した URL にアクセス
- サーバーが返したデータを response として受け取る
- .json() で JSON に変換
- 読み取ったデータを data として使える
SymbolFacadeについて
facade は、Symbol SDK(特に v3 系)で登場する 「便利な高レベル API の入口となるオブジェクト」 のことです。簡単に言うと、 facade = Symbol ブロックチェーンの処理をまとめて操作できる“窓口” もっと具体的にいうとSymbol SDK v3(TypeScript/JavaScript)では、 トランザクションの作成・署名・ハッシュ計算などの操作を 直接行うための中心的なクラス が提供されています。
facade がやってくれること
1. ✏️ トランザクションの署名
const signature = facade.signTransaction(account, tx);
2**. 🔢 トランザクションハッシュの計算**
const hash = facade.hashTransaction(signedTx);
3. 🧮 公開鍵 → アドレス
const address = facade.network.publicKeyToAddress(publicKey);
4. 📦 Payload を組み立てる
const serialized = facade.serializeTransaction(signedTx);
など、Symbol SDK の中核となる操作をすべてここから実行できます。
✨ つまり簡単に言うと…
- SDK v3 の中心部
- Symbol ネットワークでの処理をまとめた便利オブジェクト
- トランザクション作成・署名・ハッシュ・アドレス生成 などの入り口
facade=「Symbol の機能をひとまとめにして扱いやすくした道具箱」
第3章 アカウント
アカウントの作成・送信・確認
alice = facade.createAccount(new
sdkCore.PrivateKey("1E9139CC1580B4AED6A1FE110085281D4982ED0D89CE07F3380EB83069B1****
"));
注意点 newを改行した以下の書き方
new
ClassName()
は 構文エラーになるので、要注意です。
正しい書き方
① 1行にする(推奨)
alice = facade.createAccount(
new sdkCore.PrivateKey("1E9139CC1580B4AED6A1FE110085281D4982ED0D89CE07F3380EB83069B1****")
);
② 改行するなら new を分離しない
alice = facade.createAccount(
new sdkCore.PrivateKey(
"1E9139CC1580B4AED6A1FE110085281D4982ED0D89CE07F3380EB83069B1****"
)
);
ぜこのエラー文になるのか
- JS エンジンは new を読んだ瞬間に 「次はコンストラクタ名が来るはず」 と解釈
- 改行が入ることでトークン解析に失敗
- 「想定外のトークン」として SyntaxError
補足(Symbol / SDK 系でよくある罠)
- ドキュメントを Markdown や PDF からコピペ するとこうした改行が入りやすい
- 特に new / return / throw は要注意
秘密鍵に関する注意点1E9139CC1580B4AED6A1FE110085281D4982ED0D89CE07F3380EB83069B1
は16進数ではないので、秘密鍵としての形式にはなっていません。
3.4 現場で使えるヒント
暗号化
message = 'Hello Symbol!';
encryptedMessage = alice.messageEncoder().encode(bob.publicKey, new
TextEncoder().encode(message));
console.log(Buffer.from(encryptedMessage).toString("hex").toUpperCase());
この構文は Node.js 用で、ブラウザではそのまま使えません。
なぜエラーになるか(今回のコード)
console.log(Buffer.from(encryptedMessage).toString("hex").toUpperCase());
- Buffer → Node.js 専用
- ブラウザ(Chrome / Firefox)には Buffer が存在しない
その結果、エラーになる。
ブラウザ対応の正しい書き方
encryptedMessage は Uint8Array なので、自前で hex 文字列に変換します。
const message = 'Hello Symbol!';
const encryptedMessage = alice
.messageEncoder()
.encode(bob.publicKey, new TextEncoder().encode(message));
// Uint8Array → hex string
const hexMessage = Array.from(encryptedMessage)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
console.log(hexMessage);
復号化
const decryptMessageData = bob
.messageEncoder()
.tryDecode(
alice.publicKey,
hexToUint8(encryptedHex)
);
console.log("decode result:");
console.log(decryptMessageData);
if (decryptMessageData.isDecoded) {
const decryptedMessage =
new TextDecoder().decode(decryptMessageData.message);
console.log("decrypted message:");
console.log(decryptedMessage);
} else {
console.log("decrypt failed!");
}
ここまでをまとめると・・・
// ===== ユーティリティ =====
// hex文字列 → Uint8Array
function hexToUint8(hex) {
return new Uint8Array(
hex.match(/.{1,2}/g).map(b => parseInt(b, 16))
);
}
// Uint8Array → hex文字列
function uint8ToHex(uint8) {
return Array.from(uint8)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
}
// ===== アカウント準備 =====
// ※ 実際は自分の秘密鍵に置き換えてください
const alice = facade.createAccount(
new sdkCore.PrivateKey(
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
)
);
const bob = facade.createAccount(
new sdkCore.PrivateKey(
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
)
);
// ===== ① メッセージ暗号化 =====
const message = "Hello Symbol!";
const encodedMessage = new TextEncoder().encode(message);
const encryptedMessage = alice
.messageEncoder()
.encode(bob.publicKey, encodedMessage);
// Uint8Array → hex
const encryptedHex = uint8ToHex(encryptedMessage);
console.log("encrypted hex:");
console.log(encryptedHex);
// ===== ② メッセージ復号 =====
const decryptMessageData = bob
.messageEncoder()
.tryDecode(
alice.publicKey,
hexToUint8(encryptedHex)
);
console.log("decode result:");
console.log(decryptMessageData);
if (decryptMessageData.isDecoded) {
const decryptedMessage =
new TextDecoder().decode(decryptMessageData.message);
console.log("decrypted message:");
console.log(decryptedMessage);
} else {
console.log("decrypt failed!");
}
署名
const payload = new TextEncoder().encode("Hello Symbol!");
const signature = alice.keyPair.sign(payload);
console.log(signature);
検証
const verifier = new sdkSymbol.Verifier(alice.publicKey);
// メッセージ → Uint8Array
const payload = new TextEncoder().encode("Hello Symbol!");
// 署名検証
const isVerified = verifier.verify(payload, signature);
console.log(isVerified);
第4章 トランザクション
4.2.1 Bobへの転送トランザクション
messageData = "\0Hello, Symbol!"; // 平文メッセージ
// トランザクション Descriptor 設定
descriptor = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
bob.address, // 受取アドレス
[
//// 1XYM送金
// new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
// new sdkSymbol.models.UnresolvedMosaicId(0x72C0212E67A08BCEn),
// new sdkSymbol.models.Amount(1000000n)
// )
],
messageData // メッセージ
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
console.log(tx);
① メッセージの定義
messageData = "\0Hello, Symbol!"; // 平文メッセージ
- \0 は Symbolの「プレーンメッセージ(未暗号)」を示す識別子
- "Hello, Symbol!" がブロックチェーン上に記録される文字列
👉 暗号化メッセージではなく、誰でも読めるメッセージ
② TransferTransaction の Descriptor を作成
descriptor = new sdkSymbol.descriptors.TransferTransactionV1Descriptor(
bob.address, // 受取アドレス
[
// モザイク(送金資産)
],
messageData // メッセージ
);
この Descriptor は「どんな転送トランザクションか」を定義します。
各引数の意味
1️⃣ bob.address
- 受取人の Symbol アドレス
- 送金先(メッセージの宛先)
2️⃣ モザイク配列(ここでは空)
[
// new UnresolvedMosaicDescriptor(...)
]
- 本来は XYM や独自トークンを指定する場所
- コメントアウトされているため 送金額は 0
- 👉 メッセージのみ送信するトランザクション
※ コメントを外すと 1 XYM(1,000,000 microXYM)送金になる
3️⃣ messageData
- トランザクションに付与するメッセージ
- Symbolでは 送金=メッセージ送信手段としても使われる
③ トランザクションの実体を生成
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション定義
alice.publicKey, // 署名者(送信者)
100, // 手数料乗数
60 * 60 * 2 // Deadline(2時間)
);
各パラメータ
🔑 alice.publicKey
- トランザクションの 送信者
- 実際に署名するアカウント
💰 100(手数料乗数)
- 実際の手数料 = トランザクションサイズ × 100
- ノードに優先的に処理されやすくなる
⏰ 60 * 60 * 2
- トランザクションの有効期限(秒)
- = 2時間
- この時間内に承認されなければ無効
④ 結果の出力
console.log(tx);
- 作成された 未署名トランザクションオブジェクトを表示
- この後に、署名、ネットワークへアナウンスという流れになる
2XYMを送る場合 2 XYM を送る場合は、コメントアウトされている「モザイク指定」を有効化し、数量を 2,000,000(microXYM)にします。 Symbolでは 1 XYM = 1,000,000 microXYM です。
変更点(ここだけ直せばOK)
// 2 XYM = 2,000,000 microXYM
new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
new sdkSymbol.models.UnresolvedMosaicId(0x72C0212E67A08BCEn), // XYM モザイクID
new sdkSymbol.models.Amount(2000000n) // 2 XYM
)
2 XYM + メッセージ送信の完成例
messageData = "\0Hello, Symbol!"; // 平文メッセージ
descriptor = new sdkSymbol.descriptors.TransferTransactionV1Descriptor(
bob.address, // 受取アドレス
[
new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
new sdkSymbol.models.UnresolvedMosaicId(0x72C0212E67A08BCEn), // XYM
new sdkSymbol.models.Amount(2000000n) // 2 XYM
)
],
messageData
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor,
alice.publicKey,
100, // 手数料乗数
60 * 60 * 2 // 2時間
);
console.log(tx);
署名
sig = alice.signTransaction(tx);
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
この2行は、作成したトランザクションを「署名して、ネットワークに送れる完成形」にする処理です。 Symbolではこの工程が必須です。
① トランザクションに署名する
sig = alice.signTransaction(tx);
何をしている?
- tx(未署名トランザクション)に対して
- Alice の秘密鍵で
- 暗号署名(Ed25519)を作成している
ポイント
- 署名対象は「トランザクションのハッシュ」
- 秘密鍵は外に出ない
- sig は 署名データ(64byte)
👉「このトランザクションは確かに Alice が作りました」という数学的証明
② 署名をトランザクションに埋め込む
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
何をしている?
- 未署名トランザクション tx
- 作成した署名 sig
- を合体させて ノードに送信可能な JSON ペイロードを生成
生成されるもの
- jsonPayload は REST /transactions にPOSTできる形式 Symbolノードがそのまま受け取れるデータ
**処理の流れを一言で **① tx を作る(中身だけ) ② 秘密鍵で署名する(本人証明) ③ 署名を合体させる(完成品)
4.3.2 アナウンス
res = await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
console.log(res);
全体でやっていること(要約) 👉 完成したトランザクション(jsonPayload)をノードに PUT し、ノードからの受付結果(JSON)を受け取って表示する
① fetch でトランザクションを送信
res = await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
何が起きている?
- NODE → SymbolノードのURL(例:
- /transactions → トランザクション投入用エンドポイント
- PUT → Symbol REST仕様での「トランザクション送信」
- body: jsonPayload → 署名付きトランザクション本体
👉 これで**「このトランザクションをネットワークに流してください」**とノードに依頼している
② ノードのレスポンスを JSON に変換
.then((res) => res.json())
- ノードからの HTTP レスポンスを
- JavaScriptオブジェクトに変換
③ JSON をそのまま返す
.then((json) => {
return json;
});
- 実質的に .then(res => res.json()) と同じ
- 成功・失敗メッセージを res に格納
④ 結果を表示
console.log(res);
典型的な成功レスポンス
{
"message": "packet 9 was pushed to the network via /transactions"
}
👉 「ノードが受理した」だけで、まだ承認はされていない
- この時点は アナウンス成功
- ブロックに入るかは別問題
承認確認は次で行う:
/transactionStatus
/confirmed
WebSocket 監視
**処理の流れを一文で **署名済みTx → ノードに送信 → 受付結果(JSON)を受け取る
4.4.1 ステータスの確認
body = {
"hashes": facade.hashTransaction(tx).toString()
};
transactionStatus = await fetch(
new URL('/transactionStatus', NODE),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
if (transactionStatus.length <= 0) {
console.error("not exist tx.");
} else {
console.log(transactionStatus[0]);
}
このコードは、送信したトランザクションが今どんな状態にあるか(未処理/承認済み/失敗)をノードに問い合わせて確認する処理です。 Symbolでは「送った=確定」ではないため、この確認が重要です。
① トランザクションハッシュを計算
body = {
"hashes": facade.hashTransaction(tx).toString()
};
何をしている?
- tx(元のトランザクション内容)から
- トランザクションハッシュを計算
ポイント
- このハッシュは ネットワーク上での **トランザクションID **検索・追跡のキー
- 署名前ではなく 👉 トランザクション本体から一意に決まる
② /transactionStatus に問い合わせ
transactionStatus = await fetch(
new URL('/transactionStatus', NODE),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}
)
エンドポイントの意味
- /transactionStatus 未承認・失敗・期限切れを含む「現在の状態」を返す confirmed とは別
POST なのはなぜ?
- 複数ハッシュをまとめて問い合わせ可能
- 今回は1件だけ
③ レスポンスを JSON に変換
.then((res) => res.json())
返ってくる形式(例)
[
{
"group": "unconfirmed",
"code": "Success",
"hash": "ABC123..."
}
]
④ 結果の有無をチェック
if (transactionStatus.length <= 0) {
console.error("not exist tx.");
}
意味
- ノードがそのハッシュのTxをまだ見ていない / 破棄済み
- 可能性は以下である。 ・アナウンス前 ・別ノードに投げた直後 ・期限切れ後
⑤ ステータスを表示
console.log(transactionStatus[0]);
未承認
{
"group": "unconfirmed",
"code": "Success"
}
承認済み
{
"group": "confirmed",
"height": "1234567"
}
失敗
{
"group": "failed",
"code": "Failure_Core_Insufficient_Balance"
}
/transactionStatus の重要ポイント
group意味unconfirmedメモリプールにあるconfirmedブロックに入ったfailed失敗して破棄―見つからない
なぜ confirmed を直接見ない?
- 承認前は /confirmed に存在しない
- まず status → 次に confirmed が定石
**ここでまでの処理全体を一文で言うと・・・ **Txハッシュ → ノードに状態確認 → 結果を表示
4.4.2 承認確認
SDKで確認
txInfo = await fetch(
new URL('/transactions/confirmed/' + facade.hashTransaction(tx).toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
console.log(txInfo);
全体の役割(ひとことで) 👉 トランザクションハッシュを使って、確定済みTxの詳細をノードから取得する
① 取得対象のURLを組み立て
new URL(
'/transactions/confirmed/' + facade.hashTransaction(tx).toString(),
NODE
)
何をしている?
- facade.hashTransaction(tx).toString() 対象トランザクションの ハッシュ
- /transactions/confirmed/{hash} confirmed(ブロック確定)済みTx専用の取得API
👉このAPIに存在する = すでにブロックに入った
② GET で問い合わせ
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
- 読み取り専用
- ノードに保存されている確定Txを取得
③ レスポンスを JSON に変換
.then((res) => res.json())
Txの完全な内容が返る
④ 結果を表示
console.log(txInfo);
4.5 トランザクション履歴
params = new URLSearchParams({
"address": alice.address.toString(),
"embedded": true,
});
result = await fetch(
new URL('/transactions/confirmed?' + params.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
txes = result.data;
txes.forEach(tx => {
console.log(tx);
});
このコードは、Alice のアドレスが関与した「確定済み(confirmed)」トランザクション一覧を取得して、1件ずつ表示する処理です。 単発Txではなく、履歴一覧を取るためのAPIを使っています。
① クエリパラメータを作成
params = new URLSearchParams({
"address": alice.address.toString(),
"embedded": true,
});
各パラメータの意味
🔹 address
- 検索対象の Symbol アドレス
- 送信者 or 受信者として関与したTxを抽出
🔹 embedded: true
- Aggregate Transaction 内の「内包Tx」も含める
- false にすると 単独Txのみ Aggregate の中身は除外
👉 実務では true がほぼ必須
② confirmed トランザクション一覧を取得
javascript
fetch(
new URL('/transactions/confirmed?' + params.toString(), NODE),
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
)
使用しているAPI
javascript
GET /transactions/confirmed
- ブロックに確定済みのトランザクション一覧
- クエリで条件指定
③ レスポンスを JSON に変換
javascript
.then((res) => res.json())
返ってくる構造(概念)
javascript
{
"data": [
{ "meta": {...}, "transaction": {...} },
{ "meta": {...}, "transaction": {...} }
],
"pagination": {
"pageNumber": 1,
"pageSize": 10
}
}
④ トランザクション配列を取り出す
javascript
txes = result.data;
- data に確定済みトランザクションの配列
- ページングがある点に注意
⑤ 1件ずつ表示
txes.forEach(tx => {
console.log(tx);
});
- 各 tx は ・meta(ブロック情報) ・transaction(Tx本体)
- 送金・メッセージ・Aggregate 内包Txなどが混在
このコードで分かること
- Alice が ・送った ・受け取った ・Aggregate に署名した
- すべての確定済み履歴
4.6 アグリゲートトランザクション
bob = facade.createAccount(sdkCore.PrivateKey.random());
carol = facade.createAccount(sdkCore.PrivateKey.random());
// アグリゲートTxに含めるTxを作成
descriptor1 = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
bob.address, // 送信先アドレス
[], // 送信モザイク
'tx1' // 平文メッセージ
);
innerTx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor1, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
);
descriptor2 = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
carol.address, // 送信先アドレス
[], // 送信モザイク
'tx2' // 平文メッセージ
);
innerTx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor2, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
);
embeddedTransactions = [
innerTx1,
innerTx2
];
// アグリゲートTx作成
descriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
0 // 連署者数
);
// 署名とアナウンス
sig = alice.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
このコードは、Symbol の Aggregate Complete Transaction(アグリゲート完全トランザクション)を使って、 Alice が 2つの送金(Bob と Carol 宛)を「1つのトランザクションとして同時に確定」させる処理です。
送金額は 0、メッセージ付き TransferTx を2本まとめて確定しています。
全体像 Alice が 「Bob に tx1」+「Carol に tx2」 という 2つの TransferTx を、1ブロックで原子的(同時)に実行する
① Bob / Carol のアカウントを生成
bob = facade.createAccount(sdkCore.PrivateKey.random());
carol = facade.createAccount(sdkCore.PrivateKey.random());
- ランダムな秘密鍵から
- 新しいアカウントを作成
- 今回は 受信専用(署名しない)
② アグリゲートに入れる内包Tx(EmbeddedTx)を作成
🔹 Bob 宛の内包 TransferTx
descriptor1 = new TransferTransactionV1Descriptor(
bob.address,
[],
'tx1'
);
innerTx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor1,
alice.publicKey
);
- Alice が署名者
- Bob 宛
- メッセージ "tx1"
- モザイクなし
🔹 Carol 宛の内包 TransferTx
descriptor2 = new TransferTransactionV1Descriptor(
carol.address,
[],
'tx2'
);
innerTx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor2,
alice.publicKey
);
- 同じく Alice が署名者
- Carol 宛
- メッセージ "tx2"
③ 内包トランザクションを配列にまとめる
embeddedTransactions = [
innerTx1,
innerTx2
];
④ Aggregate Complete Transaction を作成
descriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
ここがアグリゲートの核心
- hashEmbeddedTransactions(...) ・内包Tx全体のハッシュ ・内容改ざん防止
- embeddedTransactions ・実行される Tx 群
⑤ アグリゲートTx本体を生成
aggregateTx = facade.createTransactionFromTypedDescriptor(
descriptor,
alice.publicKey,
100, // 手数料
60 * 60 * 2, // 2時間
0 // 連署者数
);
0 の意味
- 追加の連署者はいない
- すべて Alice 単独で完結
- → Aggregate Complete が成立
⑥ 署名
javascript
sig = alice.signTransaction(aggregateTx);
- AggregateTx 全体に Alice が署名
- 内包Txもまとめて承認
⑦ 署名を結合してアナウンス
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
await fetch('/transactions', ...)
- ネットワークに送信
- ノードが受理すれば2つの TransferTx が同時に・不可分で確定
第5章 モザイク
// モザイクフラグ設定
f = sdkSymbol.models.MosaicFlags.NONE.value;
f += sdkSymbol.models.MosaicFlags.SUPPLY_MUTABLE.value; // 供給量変更の可否
// f += sdkSymbol.models.MosaicFlags.TRANSFERABLE.value; // 第三者への譲渡可否
f += sdkSymbol.models.MosaicFlags.RESTRICTABLE.value; // 制限設定の可否
f += sdkSymbol.models.MosaicFlags.REVOKABLE.value; // 発行者からの還収可否
flags = new sdkSymbol.models.MosaicFlags(f);
// ナンス設定
array = new Uint8Array(sdkSymbol.models.MosaicNonce.SIZE);
crypto.getRandomValues(array);
nonce = sdkSymbol.models.MosaicNonce.deserialize(array);
//モザイク定義
mosaicDefDescriptor = new sdkSymbol.descriptors.MosaicDefinitionTransactionV1Descriptor( // Txタイプ:モザイク定義Tx
new sdkSymbol.models.MosaicId(sdkSymbol.generateMosaicId(alice.address, nonce.value)), // モザイクID
new sdkSymbol.models.BlockDuration(0n), // duration:有効期限
nonce, // ナンス
flags, // モザイクフラグ
2 // divisibility:可分性
);
mosaicDefTx = facade.createEmbeddedTransactionFromTypedDescriptor(
mosaicDefDescriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
);
① モザイクフラグ設定(性質の定義)
f = sdkSymbol.models.MosaicFlags.NONE.value;
f += sdkSymbol.models.MosaicFlags.SUPPLY_MUTABLE.value; // 供給量変更の可否
// f += sdkSymbol.models.MosaicFlags.TRANSFERABLE.value; // 第三者への譲渡可否
f += sdkSymbol.models.MosaicFlags.RESTRICTABLE.value; // 制限設定の可否
f += sdkSymbol.models.MosaicFlags.REVOKABLE.value; // 発行者からの還収可否
flags = new sdkSymbol.models.MosaicFlags(f);
ここは 「このモザイクがどんな振る舞いを持つか」 をビットフラグで指定しています。
| フラグ | 意味 | 今回の設定 |
| ----------------| ---------------- | ------------- |
| `SUPPLY_MUTABLE` | 発行後に供給量を増減できる| ✅ 有効 |
| `TRANSFERABLE` | 第三者に自由に送れる | ❌ 無効(コメントアウト) |
| `RESTRICTABLE` | アドレス制限などを後から設定可能 | ✅ 有効 |
| `REVOKABLE` | 発行者が強制回収できる | ✅ 有効 |
② ナンス設定(モザイクID生成用)
array = new Uint8Array(sdkSymbol.models.MosaicNonce.SIZE);
crypto.getRandomValues(array);
nonce = sdkSymbol.models.MosaicNonce.deserialize(array);
何をしているか?
- ランダムな Nonce(使い捨て値) を生成
- この nonce と発行者アドレスから 一意な MosaicId を作るため
🔍 Symbolでは
MosaicId = hash(発行者アドレス + nonce)
なので、
- 同じアドレスでも nonceが違えば別トークン
- 再現性がありつつ衝突しない
③ モザイク定義トランザクション
mosaicDefDescriptor = new sdkSymbol.descriptors.MosaicDefinitionTransactionV1Descriptor(
new sdkSymbol.models.MosaicId(
sdkSymbol.generateMosaicId(alice.address, nonce.value)
),
new sdkSymbol.models.BlockDuration(0n), // duration
nonce,
flags,
2 // divisibility
);
各引数の意味
| 項目 | 内容 |
| ----------------- | -------------------------- |
| MosaicId | alice + nonce から生成したモザイクID |
| BlockDuration(0n) | **無期限(永久トークン)** |
| nonce | さっき生成したナンス |
| flags | 上で定義したモザイクの性質 |
| divisibility = 2 | 小数点以下2桁まで扱える |
④ 埋め込みトランザクション化
mosaicDefTx = facade.createEmbeddedTransactionFromTypedDescriptor(
mosaicDefDescriptor,
alice.publicKey,
);
これは何?
- アグリゲートトランザクション用の「Embedded Transaction」を作成している
つまり:
- 単体では流さない
- 後で ・モザイク供給変更 ・初期配布 ・制限設定 などと まとめて1トランザクションにする前提
数量変更
//モザイク変更
mosaicChangeDescriptor = new sdkSymbol.descriptors.MosaicSupplyChangeTransactionV1Descriptor( // Txタイプ:モザイク変更Tx
new sdkSymbol.models.UnresolvedMosaicId(mosaicDefTx.id.value), // モザイクID
new sdkSymbol.models.Amount(10000n), // 数量
sdkSymbol.models.MosaicSupplyChangeAction.INCREASE // アクション
);
mosaicChangeTx = facade.createEmbeddedTransactionFromTypedDescriptor(
mosaicChangeDescriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
);
これは 「さっき定義したモザイクの総発行量を増やす」ための処理です。 Symbolでは モザイク定義(性質) と 供給量(サプライ) は別トランザクションなので、その後者をやっています。
第6章 ネームスペース
6.1 手数料の計算
rentalFees = await fetch(
new URL('/network/fees/rental', NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
rootNsperBlock = Number(rentalFees.effectiveRootNamespaceRentalFeePerBlock);
rentalDays = 365;
rentalBlock = rentalDays * 24 * 60 * 60 / 30;
rootNsRenatalFeeTotal = rentalBlock * rootNsperBlock;
console.log("rentalBlock:" + rentalBlock);
console.log("rootNsRenatalFeeTotal:" + rootNsRenatalFeeTotal);
① ネームスペースレンタル料金の取得
rentalFees = await fetch(
new URL('/network/fees/rental', NODE),
何をしている?
- Symbol ノードに**「現在のネームスペース利用料金」** を問い合わせ
- ネットワーク設定に依存する値なのでハードコードしないのが正解
② 1ブロックあたりの料金を数値化
rootNsperBlock =
Number(rentalFees.effectiveRootNamespaceRentalFeePerBlock);
意味
- JSONは文字列なので Number() で数値に変換
- 単位:μXYM / block
④ 借りたい日数を指定
rentalDays = 365;
👉 1年間借りたい
⑤ 日数 → ブロック数に変換
rentalBlock = rentalDays * 24 * 60 * 60 / 30;
なぜ / 30?
- Symbol の 平均ブロック生成時間 = 約30秒
- つまり1日 = 24×60×60 / 30 ≒ 2880 blocks
👉 365日分のブロック数を計算。
⑥ 総レンタル料金を計算
rootNsRenatalFeeTotal =
rentalBlock * rootNsperBlock;
意味
- (1ブロックの料金)×(利用ブロック数)
- 単位:μXYM
👉 実際にトランザクションで支払う総額。
⑦ 結果の出力
console.log("rentalBlock:" + rentalBlock);
console.log("rootNsRenatalFeeTotal:" + rootNsRenatalFeeTotal);
表示されるもの
- rentalBlock → 約 1,051,200 blocks
- rootNsRenatalFeeTotal → ルートネームスペース1年分のコスト(μXYM)
(※ これを 1,000,000 で割ると XYM)
6.2 レンタル
// ネームスペース設定
name = "xembook"; // 作成するルートネームスペース
// Tx作成
descriptor = new sdkSymbol.descriptors.NamespaceRegistrationTransactionV1Descriptor( // Txタイプ:ネームスペース登録Tx
new sdkSymbol.models.NamespaceId(sdkSymbol.generateNamespaceId(name)), // ネームスペースID
sdkSymbol.models.NamespaceRegistrationType.ROOT, // 登録タイプ : ルートネームスペース
new sdkSymbol.models.BlockDuration(86400n), // duration:レンタル期間
undefined, // 親ネームスペース (ルートの場合は省略可)
name // ネームスペース
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名とアナウンス
sig = alice.signTransaction(tx);
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
① 作成するネームスペース名
name = "xembook";
意味
- 取得したい ルートネームスペース
② ネームスペース登録トランザクション
descriptor =
new sdkSymbol.descriptors.NamespaceRegistrationTransactionV1Descriptor(
ネームスペースを借りる(登録する)Tx
③ ネームスペースID生成
new sdkSymbol.models.NamespaceId(
sdkSymbol.generateNamespaceId(name)
)
何をしている?
- "xembook" という文字列から決定論的に NamespaceId を生成
👉 誰が計算しても同じID 👉 文字列とIDが 1:1 対応
④ 登録タイプ:ROOT
sdkSymbol.models.NamespaceRegistrationType.ROOT
意味
- ルートネームスペース
- 親を持たない最上位
(サブなら CHILD + 親IDが必要)
⑤ レンタル期間(BlockDuration)
new sdkSymbol.models.BlockDuration(86400n)
重要ポイント
- 単位は ブロック数
- 86400 blocks ≒ 約30日(30秒/ブロック換算)
👉 この期間だけ利用可能
期限が切れると:
- 自動失効
- 誰でも再取得可能
⑥ 親ネームスペース
- ルートなので不要
- サブネームスペースの場合のみ指定
⑦ ネームスペース名
name
意味
- 実際にチェーン上に登録される文字列
- Explorer等で表示される
有効期限の計算
namespaceIds = sdkSymbol.generateNamespacePath("xembook"); // ルートネームスペース
namespaceId = namespaceIds[namespaceIds.length - 1];
nsInfo = await fetch(
new URL('/namespaces/' + namespaceId.toString(16).toUpperCase(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json.namespace;
});
chainInfo = await fetch(
new URL('/chain/info', NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
lastHeight = chainInfo.height;
lastBlock = await fetch(
new URL('/blocks/' + lastHeight, NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json.block;
});
remainHeight = nsInfo.endHeight - lastHeight;
endDate = new Date(Number(lastBlock.timestamp) + remainHeight * 30000 + epochAdjustment * 1000);
console.log(endDate);
① ネームスペースIDの取得
namespaceIds = sdkSymbol.generateNamespacePath("xembook"); // ルートネームスペース
namespaceId = namespaceIds[namespaceIds.length - 1];
意味
- "xembook" という名前から ネームスペースID(数値)を計算
- generateNamespacePath は ルート → サブ → 孫 … という階層パスの配列を返す
- ルートネームスペースの場合でも配列になるため、 最後の要素が実際に使う NamespaceId
📌 REST API は 文字列名ではなく NamespaceId で問い合わせるため必要
② ネームスペース情報の取得
nsInfo = await fetch(
new URL('/namespaces/' + namespaceId.toString(16).toUpperCase(), NODE),
)
.then((res) => res.json())
.then((json) => {
return json.namespace;
});
意味
ノードに対してこのネームスペースの状態を教えてと問い合わせています。
③ 現在のチェーン状態(最新ブロック高)を取得
chainInfo = await fetch(
new URL('/chain/info', NODE),
)
.then((res) => res.json());
lastHeight = chainInfo.height;
意味
- 今の 最新ブロック高(height) を取得
- 例: lastHeight = 12,345,678
📌 ネームスペースの endHeight と比較するために必要
④ 最新ブロックの詳細を取得
lastBlock = await fetch(
new URL('/blocks/' + lastHeight, NODE),
)
.then((res) => res.json())
.then((json) => {
return json.block;
});
最新ブロックの timestamp(チェーン時間) を取得
Symbolの timestamp は**「UNIX時間ではない」** → エポック調整が必要
⑤ 残りブロック数を計算
remainHeight = nsInfo.endHeight - lastHeight;
意味
- ネームスペースが あと何ブロック生きているか
⑥ 有効期限を「日時」に変換
endDate = new Date(
Number(lastBlock.timestamp)
+ remainHeight * 30000
+ epochAdjustment * 1000
);
console.log(endDate);
ここが一番重要 👇
各要素の意味
- lastBlock.timestamp チェーン基準の現在時刻(ミリ秒ではない)
- remainHeight * 30000 Symbolは 1ブロック ≒ 30秒
- epochAdjustment * 1000 Symbol独自エポック → UNIX時間への補正
👉 これで
「このネームスペースは 現実世界の何年何月何日何時に失効するか」
が計算できる
6.3 リンク
// リンクするネームスペースとアドレスの設定
namespaceIds = sdkSymbol.generateNamespacePath("xembook");
namespaceId = new sdkSymbol.models.NamespaceId(namespaceIds[namespaceIds.length - 1]);
address = new sdkSymbol.Address("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ");
// Tx作成
descriptor = new sdkSymbol.descriptors.AddressAliasTransactionV1Descriptor( // Txタイプ:アドレスエイリアスTx
namespaceId, // ネームスペースID
address, // リンク先アドレス
sdkSymbol.models.AliasAction.LINK
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名とアナウンス
sig = alice.signTransaction(tx);
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
意味
- "xembook" という名前を 内部ID(uint64)に変換
- Symbolでは 人間が読む名前 ≠ チェーンが扱うID
- RESTやTxでは 必ず NamespaceId を使う
② リンク先アドレスの指定
address = new sdkSymbol.Address(
"TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ"
);
意味
- ネームスペースが 指し示す先の実体
- これは 通常の Symbol アドレス(T = テストネット)
- 以後: xembook を解決すると このアドレスが返るようになる
③ AddressAliasTransaction の作成
javascript
descriptor = new sdkSymbol.descriptors.AddressAliasTransactionV1Descriptor(
namespaceId,
address,
sdkSymbol.models.AliasAction.LINK
);
意味
これは:
このネームスペースを、このアドレスにリンクする
という命令。
リンク(UNLINKなら解除)
📌 ネームスペースは
- アドレス
- モザイク のどちらか一方にだけリンク可能
7.メタデータ
7.1 アカウントに登録
// ターゲットと作成者アドレスの設定
targetAddress = alice.address; // メタデータ記録先アドレス
sourceAddress = alice.address; // メタデータ作成者アドレス
// キーと値の設定
key = sdkSymbol.metadataGenerateKey("key_account");
value = new TextEncoder().encode("test");
// 同じキーのメタデータが登録されているか確認
query = new URLSearchParams({
"targetAddress": targetAddress.toString(),
"sourceAddress": sourceAddress.toString(),
"scopedMetadataKey": key.toString(16).toUpperCase(),
"metadataType": 0
});
metadataInfo = await fetch(
new URL('/metadata?' + query.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json.data;
});
// 登録済の場合は差分データを作成する
sizeDelta = value.length;
if (metadataInfo.length > 0) {
sizeDelta -= metadataInfo[0].metadataEntry.valueSize;
value = sdkSymbol.metadataUpdateValue(sdkCore.utils.hexToUint8(metadataInfo[0].metadataEntry.value), value);
}
// アカウントメタデータ登録Tx作成
descriptor = new sdkSymbol.descriptors.AccountMetadataTransactionV1Descriptor( // Txタイプ:アカウントメタデータ登録Tx
targetAddress, // ターゲットアドレス
key, // キー
sizeDelta, // サイズ差分
value // 値
);
tx = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
);
embeddedTransactions = [
tx
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
0 // 連署者数
);
// 署名とアナウンス
sig = alice.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
主な処理の流れを解説します。
1. 事前準備と既存データの確認
targetAddress = alice.address; // メタデータ記録先アドレス
sourceAddress = alice.address; // メタデータ作成者アドレス
これは、aliceに格納されているアドレスに自分自身のアカウントにメタデータを書く というケースです。
次に誰に対して(targetAddress)、どんなキー(key)でデータを書き込むかを設定し、既にそのデータが存在するかをAPIで確認しています。
key = sdkSymbol.metadataGenerateKey("key_account");
value = new TextEncoder().encode("test");
- キーの生成: metadataGenerateKey を使って、文字列("key_account")からSymbol特有の形式のキー(uint64)に変換しています。
- 既存データの取得: fetch を使ってノードに問い合わせ、既に値が登録されているかチェックします。
👉つまり「key_account = test」というメタデータを書きたい、という意味。
2. 更新用データの作成
Symbolのメタデータ更新は「現在の値」と「新しい値」の差分を書き込む形式です。 まず、以下のコードで既に同じメタデータがあるか確認する。
query = new URLSearchParams({
"targetAddress": targetAddress.toString(),
"sourceAddress": sourceAddress.toString(),
"scopedMetadataKey": key.toString(16).toUpperCase(),
"metadataType": 0
});
新規 or 更新かで value と sizeDelta を調整
sizeDelta = value.length;
if (metadataInfo.length > 0) {
sizeDelta -= metadataInfo[0].metadataEntry.valueSize;
value = sdkSymbol.metadataUpdateValue(
sdkCore.utils.hexToUint8(metadataInfo[0].metadataEntry.value),
value
);
}
- 新規登録の場合: 既存データがないため、そのままの値を書き込みます。
- 更新(Update)の場合: 1. sizeDelta: 新しい値と古い値のサイズ差分を計算します。 2. metadataUpdateValue: 重要です。 既存の値と新しい値を比較(XOR演算)して、更新に必要な「差分データ」を生成します。
3. トランザクションの構築
Symbolではメタデータの書き込みに「アグリゲート・トランザクション」を利用するのが一般的です。
- AccountMetadataTransaction: メタデータを書き込む具体的な内容(宛先、キー、サイズ差分、差分データ)を定義します。
descriptor = new sdkSymbol.descriptors.AccountMetadataTransactionV1Descriptor(
targetAddress,
key,
sizeDelta,
value
);
- EmbeddedTransaction: メタデータTxを「内包用」に変換します。
tx = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor,
alice.publicKey,
);
- AggregateCompleteTransaction: 内包したTxを包み込み、一つの大きなトランザクションとしてまとめます。
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
4. 署名とネットワークへの送信
最後に、作成したトランザクションに自分の秘密鍵で署名を行い、RESTノードを通じてブロックチェーンネットワークに送信(アナウンス)しています。
sig = alice.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
まとめ:このコードがやっていること
「特定のカウントに対して、指定したキーで 'test' という文字列をメタデータとして登録する。もし既にデータがあれば、それを正しく 'test' に書き換えるための差分計算を行ってから送信する。」
という処理です。
7.3 ネームスペースに登録
// ターゲットと作成者アドレスの設定
namespaceIds = sdkSymbol.generateNamespacePath("xembook"); // メタデータ記録先ネームスペース
targetNamespace = namespaceIds[namespaceIds.length - 1];
namespaceInfo = await fetch(
new URL('/namespaces/' + targetNamespace.toString(16).toUpperCase(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
targetAddress = new sdkSymbol.Address(sdkCore.utils.hexToUint8(namespaceInfo.namespace.ownerAddress)); // ネームスペース作成者アドレス
// キーと値の設定
key = sdkSymbol.metadataGenerateKey("key_namespace");
value = new TextEncoder().encode("test");
// 同じキーのメタデータが登録されているか確認
query = new URLSearchParams({
"targetId": targetNamespace.toString(16).toUpperCase(),
"sourceAddress": targetAddress.toString(),
"scopedMetadataKey": key.toString(16).toUpperCase(),
"metadataType": 2
});
metadataInfo = await fetch(
new URL('/metadata?' + query.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json.data;
});
// 登録済の場合は差分データを作成する
sizeDelta = value.length;
if (metadataInfo.length > 0) {
sizeDelta -= metadataInfo[0].metadataEntry.valueSize;
value = sdkSymbol.metadataUpdateValue(sdkCore.utils.hexToUint8(metadataInfo[0].metadataEntry.value), value);
}
// ネームスペースメタデータ登録Tx作成
descriptor = new sdkSymbol.descriptors.NamespaceMetadataTransactionV1Descriptor( // Txタイプ:アカウントメタデータ登録Tx
targetAddress, // ターゲットアドレス
key, // キー
new sdkSymbol.models.NamespaceId(targetNamespace), // メタデータ記録先モザイク
sizeDelta, // サイズ差分
value // 値
);
tx = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
);
embeddedTransactions = [
tx
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
0 // 連署者数
);
// 署名とアナウンス
sig = alice.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
① モザイク情報の検索
query = new URLSearchParams({
targetId: targetNamespace.toString(16).toUpperCase(),
sourceAddress: targetAddress.toString(),
scopedMetadataKey: key.toString(16).toUpperCase(),
metadataType: 2
});
指定されたモザイクIDが存在するか、その所有者は誰かをノードに問い合わせます。
注意: モザイクの所有者(targetAddress)と、このTxを送る人(alice)が一致していないと、通常このトランザクションは失敗します。
② 既存データの差分計算(XOR)
sizeDelta = value.length;
if (metadataInfo.length > 0) {
sizeDelta -= metadataInfo[0].metadataEntry.valueSize;
value = sdkSymbol.metadataUpdateValue(
oldValue,
value
);
}
- 新規: そのまま 'test' を書き込む。
- 更新: 既存の値を読み出し、新しい値との「差分」を計算して、その差分だけを送信する。
③ アグリゲート・トランザクションで送信
モザイクメタデータの更新Txをアグリゲートトランザクションに包んで送信します。
8.ロック
Symbolブロックチェーンにはハッシュロックとシークレットロックの2種類のロック機構があります。
8.1 ハッシュロック
アグリゲートボンデッドトランザクションの作成
bob = facade.createAccount(sdkCore.PrivateKey.random());
namespaceIds = sdkSymbol.generateNamespacePath("symbol.xym");
namespaceId = namespaceIds[namespaceIds.length - 1];
// アグリゲートTxに含めるTxを作成
descriptor1 = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
bob.address, // Bobへの送信
[
// 1XYM送金
new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
new sdkSymbol.models.UnresolvedMosaicId(namespaceId),
new sdkSymbol.models.Amount(1000000n)
)
],
new Uint8Array() // メッセージ無し
);
tx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor1, // トランザクション Descriptor 設定
alice.publicKey // Aliceから
);
descriptor2 = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
alice.address, // Aliceへの送信
[],
new TextEncoder('utf-8').encode('thank you!') // 平文メッセージ
);
tx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor2, // トランザクション Descriptor 設定
bob.publicKey // Bobから
);
embeddedTransactions = [
tx1,
tx2
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateBondedTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
1 // 連署者数
);
// 署名
sig = alice.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
これは「Aliceが1 XYMを送る代わりに、Bobがメッセージを返す」という2つの取引を、1つのセットにして送信しようとしています。
主な処理の流れ
1. 新しいアカウント(Bob)の作成とID準備
- bob: ランダムな秘密鍵で新しいアカウントを生成しています(テスト用などの一時的な受信者想定)。
- namespaceId: symbol.xym というネームスペース(ドメイン)から、XYMのモザイクIDを取得しています。
2. 2つの取引(内包トランザクション)を定義
ここがこのコードの肝です。2つの取引をセットにしています。
- Tx1(Alice → Bob): AliceがBobへ 1,000,000 micro-XYM(= 1 XYM)を送金する設定です。
descriptor1 = new TransferTransactionV1Descriptor(
bob.address,
[
new UnresolvedMosaicDescriptor(
new UnresolvedMosaicId(namespaceId),
new Amount(1000000n)
)
],
new Uint8Array()
);
javascript
tx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor1,
alice.publicKey
);
- Tx2(Bob → Alice): BobがAliceへ "thank you!" というメッセージ を送る設定です(送金モザイクは空 [])。
descriptor2 = new TransferTransactionV1Descriptor(
alice.address,
[],
new TextEncoder('utf-8').encode('thank you!')
);
tx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor2,
bob.publicKey
);
以上の2つのトランザクションを1つに束ねる
embeddedTransactions = [ tx1, tx2 ];
3. 「アグリゲート・ボンデッド」として構築
通常の取引(Complete)ではなく、Bonded(保留状態) として作成されています。
aggregateDescriptor = new AggregateBondedTransactionV3Descriptor(
hashEmbeddedTransactions(...),
embeddedTransactions
);
- AggregateBondedTransactionV3Descriptor:これは「誰かの同意(連署)が必要な取引」を意味します。今回のTx2はBobの署名が必要ですが、まだ署名されていないため、一旦ネットワーク上に「保留」として投げます。
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor,
alice.publicKey,
100,
deadline,
1 // 連署者数
);
- 連署者数: 1: Bobの署名が1つ足りない状態であることを示しています。
このコードの意味(ストーリー)
このコードが実行されると、以下のような状態になります。
- 提案: Aliceが「私は1 XYMあげるから、Bobは『thank you!』ってメッセージをちょうだい」というセットをネットワークに提示します。
- 保留: この取引はすぐには実行されず、ネットワーク内の「ハッシュロック(ロック用資金の供出)」を経て、Bobの同意を待つ状態になります。
- 完了: Bobがこの内容を確認して承認(連署)すると、「1 XYMの送金」と「メッセージの送信」が同時に(不可分に)実行されます。
注意: このコード単体では、まだ「ハッシュロック(Hash Lock)」の処理が含まれていません。Bonded Txをネットワークに受け入れてもらうには、事前に10 XYMを一時的に預けるハッシュロックTxを送信する必要があります。
8.2 シークレットロック・シークレットプルーフ
この仕組みの面白いところは、「AliceがBobに直接合言葉を教えなくても、Bobがどこかで合言葉を手に入れさえすれば、安全に取引が成立する」という点です。
シークレットロック
sha3_256 = (await import('https://cdn.skypack.dev/@noble/hashes/sha3')).sha3_256;
proof = crypto.getRandomValues(new Uint8Array(20)); // 解除用キーワード
hash = sha3_256.create();
hash.update(proof);
secret = hash.digest(); // ロック用キーワード
console.log("secret:" + sdkCore.utils.uint8ToHex(secret));
console.log("proof:" + sdkCore.utils.uint8ToHex(proof));
1. ハッシュ関数の準備
sha3_256 = (await import('https://cdn.skypack.dev/@noble/hashes/sha3')).sha3_256;
Symbolで標準的に使われる暗号化技術「SHA3-256」という計算ルールを読み込んでいます。
2. 解除用キーワード(Proof)の生成
proof = crypto.getRandomValues(new Uint8Array(20));
ランダムな20バイトのデータを生成しています。これが「秘密の合言葉」になります。この値を知っている人だけが、後でロックされた資産を取り出すことができます。
3. ロック用キーワード(Secret)の作成
hash = sha3_256.create();
hash.update(proof);
secret = hash.digest();
「秘密の合言葉(Proof)」をハッシュ計算にかけます。
- ここで生成された secret が「錠前」になります。
- ハッシュ関数の特徴: proof から secret を作るのは一瞬ですが、逆に secret から元の proof を当てるのは現代のコンピュータでは不可能です
9.マルチシグ化
9.1 マルチシグの登録
// マルチシグ設定Tx作成
multisigDescriptor = new sdkSymbol.descriptors.MultisigAccountModificationTransactionV1Descriptor( // Txタイプ:マルチシグ設定Tx
3, // minRemoval:除名のために必要な最小署名者数増分
3, // minApproval:承認のために必要な最小署名者数増分
[ // 追加対象アドレスリスト
carol1.address,
carol2.address,
carol3.address,
carol4.address,
],
[] // 除名対象アドレスリスト
);
multisigTx = facade.createEmbeddedTransactionFromTypedDescriptor(
multisigDescriptor, // トランザクション Descriptor 設定
bob.publicKey, // マルチシグ化したいアカウントの公開鍵を指定
);
embeddedTransactions = [
multisigTx
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
bob.publicKey, // マルチシグ化したいアカウントの公開鍵を指定
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
4 // 連署者数
);
// マルチシグ化したいアカウントによる署名
sig = bob.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
// 追加・除外対象として指定したアカウントによる連署
coSig1 = carol1.cosignTransaction(aggregateTx, false);
aggregateTx.cosignatures.push(coSig1);
coSig2 = carol2.cosignTransaction(aggregateTx, false);
aggregateTx.cosignatures.push(coSig2);
coSig3 = carol3.cosignTransaction(aggregateTx, false);
aggregateTx.cosignatures.push(coSig3);
coSig4 = carol4.cosignTransaction(aggregateTx, false);
aggregateTx.cosignatures.push(coSig4);
// アナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({"payload": sdkCore.utils.uint8ToHex(aggregateTx.serialize())}),
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
💡 コードの主な流れと意味
この処理は大きく分けて3つのステップで行われています。
1. マルチシグのルールを決める(Descriptor作成)
multisigDescriptor = new sdkSymbol.descriptors.MultisigAccountModificationTransactionV1Descriptor(
3, // minRemoval
3, // minApproval
[carol1.address, carol2.address, carol3.address, carol4.address],
[]
);
MultisigAccountModificationTransactionV1Descriptor を使い、以下の設定を行っています。
- 追加される署名者(連署者): Carol1, Carol2, Carol3, Carol4 の4人。
- minApproval (3): 送金などのトランザクションを承認するために必要な最小人数(今回は3人)。
- minRemoval (3): 連署者を脱退させるなどの設定変更に必要な最小人数(今回は3人)。
2. 手続きをひとまとめにする(アグリゲートTx作成)
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
Symbolでは、マルチシグ化の設定を「アグリゲート・コンプリート・トランザクション」という箱に入れます。
- 内包されたTx: 「Bobをマルチシグ化し、Carolたちを連署者にする」という内容。
- 手数料・期限: 手数料倍率や、2時間(60602)という有効期限を設定しています。
3. 全員の同意を得てネットワークへ流す(署名とアナウンス)
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
body: JSON.stringify({
payload: sdkCore.utils.uint8ToHex(aggregateTx.serialize())
}),
}
)
マルチシグ化には「マルチシグになる本人」と「連署者に指名された全員」の同意が必要です。
- 本人の署名: Bobが署名。
- 連署者の署名: Carol1〜4の4人全員が cosignTransaction で連署。
- アナウンス: 全員の署名が揃ったデータをノード(サーバー)に送信し、ブロックチェーンに記録します。
9.3 マルチシグ署名
アグリゲートコンプリートトランザクションで送信
namespaceIds = sdkSymbol.generateNamespacePath("symbol.xym");
namespaceId = namespaceIds[namespaceIds.length - 1];
// アグリゲートTxに含めるTxを作成
descriptor = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
alice.address.toString(), // 受取アドレス
[
// 1XYM送金
new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
new sdkSymbol.models.UnresolvedMosaicId(namespaceId),
new sdkSymbol.models.Amount(1000000n)
)
],
'\0test' // 平文メッセージ
);
tx = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
bob.publicKey, // マルチシグ化したアカウントの公開鍵
);
embeddedTransactions = [
tx
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
carol1.publicKey, // 起案者アカウントの公開鍵を指定
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
2 // 連署者数
);
// 起案者アカウントによる署名
sig = carol1.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
// 連署者アカウントによる連署
coSig2 = carol2.cosignTransaction(aggregateTx, false);
aggregateTx.cosignatures.push(coSig2);
coSig3 = carol3.cosignTransaction(aggregateTx, false);
aggregateTx.cosignatures.push(coSig3);
// アナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({"payload": sdkCore.utils.uint8ToHex(aggregateTx.serialize())}),
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
💡 コードの主な流れと意味
1. 「BobからAliceへ1 XYM送る」という計画を立てる
まず、送金の内容(中身)を定義しています。
- 送信元: Bob(bob.publicKey)
- 送信先: Alice(alice.address)
- 送るもの: 1,000,000 micro-XYM(=1 XYM)
- メッセージ: test
- 形式: createEmbeddedTransaction(アグリゲートTxの中に埋め込む専用の形式)
2. アグリゲートTxを組み立てる
Symbolではマルチシグの操作をアグリゲートTxという「封筒」に入れます。
- 起案者(Issuer): Carol1。この取引の手数料を支払い、手続きを開始する責任者です。
- 連署者数(2): 追加で必要な署名者の数。
3. 連署(Cosignature)を集める
Bobのアカウントを動かすためには、前回の設定(4人中3人の承認が必要)を満たす必要があります。
- 署名1: Carol1(起案者としての署名)
- 署名2: Carol2(連署)
- 署名3: Carol3(連署) これで合計 3名 の署名が揃いました。
4. ネットワークへ送信
全員の署名が揃った状態でノードに送られ、承認されればBobのアカウントからAliceへ送金が実行されます。
アグリゲートボンデッドトランザクションで送信
namespaceIds = sdkSymbol.generateNamespacePath("symbol.xym");
namespaceId = namespaceIds[namespaceIds.length - 1];
// アグリゲートTxに含めるTxを作成
descriptor = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
alice.address.toString(), // Aliceへの送信
[
// 1XYM送金
new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
new sdkSymbol.models.UnresolvedMosaicId(namespaceId),
new sdkSymbol.models.Amount(1000000n)
)
],
'\0test' // 平文メッセージ
);
tx = facade.createEmbeddedTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
bob.publicKey, // マルチシグ化したアカウントの公開鍵
);
embeddedTransactions = [
tx
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateBondedTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
carol1.publicKey, // 起案者アカウントの公開鍵を指定
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
2 // 連署者数
);
// 署名
sig = carol1.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
// ハッシュロックTx作成
hashLockDescriptor = new sdkSymbol.descriptors.HashLockTransactionV1Descriptor( // Txタイプ:ハッシュロックTx
new sdkSymbol.descriptors.UnresolvedMosaicDescriptor(
new sdkSymbol.models.UnresolvedMosaicId(namespaceId), // UnresolvedMosaic:未解決モザイク
new sdkSymbol.models.Amount(10n * 1000000n) // 10xym固定値
),
new sdkSymbol.models.BlockDuration(480n), // ロック有効期限
facade.hashTransaction(aggregateTx) // アグリゲートトランザクションのハッシュ値を登録
);
hashLockTx = facade.createTransactionFromTypedDescriptor(
hashLockDescriptor, // トランザクション Descriptor 設定
carol1.publicKey, // 起案者アカウントの公開鍵を指定
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名
hashLockSig = carol1.signTransaction(hashLockTx);
hashLockJsonPayload = facade.transactionFactory.static.attachSignature(hashLockTx, hashLockSig);
// ハッシュロックTXをアナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: hashLockJsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
//ハッシュロックの承認を確認した後、ボンデッドTXをアナウンス
await fetch(
new URL('/transactions/partial', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
💡 コードの主な流れと意味
1. アグリゲート・ボンデッド(未完成の封筒)の作成
AggregateBondedTransactionV3Descriptor を使用しています。これは「署名が足りないけれど、後で集めるので一旦受け付けてください」という種類の取引です。
- 内容: BobのアカウントからAliceへ 1 XYM 送金。
- 署名者: 現時点では起案者(Carol1)の署名のみ。
2. ハッシュロック(スパム防止の預け金)の作成
Symbolのルールでは、未完成の取引(ボンデッド)をネットワークに載せる際、「10 XYM」を一時的に担保として預ける必要があります。これを ハッシュロック と呼びます。
- 目的: 無差別な未完成取引の連投(スパム)を防ぐため。
- 期間: 約480ブロック(約15時間)以内に署名が集まれば、この10 XYMは返却されます。
3. 二段階のアナウンス
ここが最大のポイントです。
- まずハッシュロックを送信: /transactions(通常エンドポイント)へ送り、ネットワークに「10 XYM 預けます」と伝えます。
- 次にボンデッドTxを送信: ハッシュロックが承認された後、/transactions/partial(パーシャル:一部という意味)という専用のエンドポイントへ送ります。
10.監視
SymbolのノードはWebSocket通信でブロックチェーンの状態変化を監視することが可能です。 WebSocket通信とは、Webブラウザ(クライアント)とサーバーの間で、「一度つなげたら、ずっとつなぎっぱなし」にする通信規格のことです。 Symbolなどのブロックチェーンにおいて、トランザクションが承認されたかどうかをリアルタイムで検知するために欠かせない技術です。
10.1 リスナー設定
WebSocketを生成してリスナーの設定を行います。 Symbolにおけるリスナーとは、一言で言えば「ブロックチェーン上で起こるイベントをずっと見張っている監視員」のことです。 先ほどのWebSocket通信を利用して、「特定の出来事(送金の着金や承認など)」が発生した瞬間に、あらかじめ決めておいた処理を実行してくれるプログラムの部品を指します。
// チャンネル名
ListenerChannelName = {
block: 'block',
confirmedAdded: 'confirmedAdded',
unconfirmedAdded: 'unconfirmedAdded',
unconfirmedRemoved: 'unconfirmedRemoved',
partialAdded: 'partialAdded',
partialRemoved: 'partialRemoved',
cosignature: 'cosignature',
modifyMultisigAccount: 'modifyMultisigAccount',
status: 'status',
finalizedBlock: 'finalizedBlock',
}
// 各種設定
wsEndpoint = NODE.replace('http', 'ws') + "/ws"; // WebSocketエンドポイント設定
uid = "";
funcs = {};
// チャンネルへのコールバック追加
addCallback = (channel, callback) => {
if (!funcs.hasOwnProperty(channel)) {
funcs[channel] = [];
}
funcs[channel].push(callback);
};
// WebSocket初期化
listener = new WebSocket(wsEndpoint);
// メッセージ受信時処理
listener.onmessage = function(e) {
// 受信データをJSON変換
data = JSON.parse(e.data);
// WebSocket初期化後、ノードから uid を渡されるため保持しておく
if (data.uid != undefined) {
uid = data.uid;
return;
}
// subscribe しているチャンネルであればコールバックを実行する
if (funcs.hasOwnProperty(data.topic)) {
funcs[data.topic].forEach(f => {
f(data.data);
});
}
};
// エラー時処理
listener.onerror = function(error) {
console.error(error.data);
};
// クローズ時処理
listener.onclose = function(closeEvent) {
uid = "";
funcs = {};
console.log(closeEvent);
};
このコードは、Symbol SDKが内部で行っている「リスナー(監視員)の仕組み」を自前で実装したものです。SDKの便利な機能(listener.open() など)を使わずに、生のWebSocketを使って「ノードとの接続から、情報の振り分け」までをゼロから構築しています。いわば、「監視システムの心臓部」を作っているコードです。
💡 コードの主な役割と流れ
1. 監視ポイント(チャンネル)の定義
ListenerChannelName = {
block: 'block',
confirmedAdded: 'confirmedAdded',
unconfirmedAdded: 'unconfirmedAdded',
unconfirmedRemoved: 'unconfirmedRemoved',
partialAdded: 'partialAdded',
partialRemoved: 'partialRemoved',
cosignature: 'cosignature',
modifyMultisigAccount: 'modifyMultisigAccount',
status: 'status',
finalizedBlock: 'finalizedBlock',
}
Symbol WebSocket が送ってくる イベント種別一覧。
特に重要なのは👇
- confirmedAdded → Tx がブロックに入った
- partialAdded → Aggregate Bonded が公開された
- cosignature → 誰かが連署した
- status → エラー・期限切れ
- finalizedBlock → ファイナライズ完了(確定中の確定)
2 WebSocket接続先の設定
wsEndpoint = NODE.replace('http', 'ws') + "/ws";
意味 REST ノード URL を**WebSocket 用 URL に変換 **例:http://node:3001 → ws://node:3001/ws
3 内部状態変数
uid = "";
funcs = {};
- uid → ノードが割り当てる接続ID → subscribe 時に必須
- funcs → { confirmedAdded: [callback1, callback2], partialAdded: [callback3] }
みたいな イベント別コールバック置き場
4. コールバック(後出し指示)の登録
addCallback = (channel, callback) => {
if (!funcs.hasOwnProperty(channel)) {
funcs[channel] = [];
}
funcs[channel].push(callback);
};
addCallback 関数は、「特定のニュースが届いた時に、どのプログラムを実行するか」をあらかじめ登録しておく箱を作っています。
例:「confirmedAdded(承認)が届いたら、画面の『処理中...』を『成功!』に書き換える」といった予約。
5 WebSocket初期化
listener = new WebSocket(wsEndpoint);
👉 ノードと常時接続開始
6. . メッセージの振り分け処理 (onmessage)
listener.onmessage = function(e) {
data = JSON.parse(e.data);
ここが最も重要な部分です。ノードから大量に流れてくるデータを見て、
- data.uid が入っていたら、まずはそれを記録。
- data.topic(どのチャンネルのニュースか)を見て、登録されていた関数(コールバック)を次々と実行します。
7. uid(会員番号のようなもの)の取得
if (data.uid != undefined) {
uid = data.uid;
return;
}
WebSocketで接続すると、ノードからまず uid という識別子が送られてきます。 これがないと、ノードに対して「私にこのアドレスの情報を教えて!」というリクエスト(subscribe)が送れません。
8. イベント振り分け
if (funcs.hasOwnProperty(data.topic)) {
funcs[data.topic].forEach(f => {
f(data.data);
});
}
ここが最も重要な部分です。ノードから大量に流れてくるデータを見て、
- data.uid が入っていたら、まずはそれを記録。
- data.topic(どのチャンネルのニュースか)を見て、登録されていた関数(コールバック)を次々と実行します。
意味
- data.topic = イベント名(confirmedAdded など)
- 登録されている callback を全部実行
- イベント → 処理を自動で紐づけ
9 エラー処理
listener.onerror = function(error) {
console.error(error.data);
};
- WebSocket 通信エラー
- ノード切断など
10 クローズ時処理
listener.onclose = function(closeEvent) {
uid = "";
funcs = {};
console.log(closeEvent);
};
意味
- 接続が切れたらuid を破棄 登録コールバックも初期化
- 再接続前提の安全設計
10.2 受信検知
// 承認トランザクション検知時の処理
channelName = ListenerChannelName.confirmedAdded + "/" + alice.address.toString();
addCallback(channelName, (tx) => {
console.log("confirmed added", tx);
});
// 承認トランザクション検知設定
listener.send(JSON.stringify({
uid: uid,
subscribe: channelName,
}));
// 未承認トランザクション検知時の処理
channelName = ListenerChannelName.unconfirmedAdded + "/" + alice.address.toString();
addCallback(channelName, (tx) => {
console.log("unconfirmed added", tx);
});
// 未承認トランザクション検知設定
listener.send(JSON.stringify({
uid: uid,
subscribe: channelName,
}));
💡 コードの主な役割と意味
大きく分けて、2つの「監視(サブスクライブ)」をセットアップしています。
1. 「承認(Confirmed)」の監視
取引がブロックに取り込まれ、確定した瞬間をキャッチします。
- チャンネル作成: confirmedAdded/Aliceのアドレス という専用のパスを作成。
- 処理予約(addCallback): 「確定ニュースが届いたら、コンソールに中身を表示してね」と予約。
- ノードへ依頼(listener.send): 先ほど取得した uid を添えて、「このアドレスの確定情報を送ってください」と公式にリクエスト。
2. 「未承認(Unconfirmed)」の監視
誰かがAliceに送金した瞬間(まだブロックには入っていないが、順番待ちリストに載った状態)をキャッチします。
- メリット: ブロックに取り込まれる前の「0承認」の段階で、即座に画面を反応させることができます(例:「送金を受け付けました。確定までお待ちください...」という表示など)。
10.3 ブロック監視
// ブロック生成検知時の処理
addCallback(ListenerChannelName.block, (block) => {
console.log("block", block);
});
// ブロック生成検知設定
listener.send(JSON.stringify({
uid: uid,
subscribe: ListenerChannelName.block,
}));
💡 コードの主な役割と意味
1. 全体の更新をキャッチする
これまでのコードは「Aliceのアドレス」といった特定の場所を監視していましたが、今回は ListenerChannelName.block を指定しています。これにより、ネットワークのどこかで誰かが何をしていようと、新しいブロック(データの塊)がチェーンに繋がれるたびに通知が届きます。
2. 処理の流れ
- 登録 (addCallback): 新しいブロックが誕生したニュースが届いたら、そのブロックの情報(高さ、タイムスタンプなど)をコンソールに表示するよう予約します。
- 依頼 (listener.send): ノードに対して「ブロック生成のニュースを購読(Subscribe)します」と伝えます。
11.制限
11.1 アカウント制限
指定アドレスからの受信制限・指定アドレスへの送信制限
bob = facade.createAccount(sdkCore.PrivateKey.random());
// 制限設定
f = sdkSymbol.models.AccountRestrictionFlags.ADDRESS.value; // アドレス制限
f += sdkSymbol.models.AccountRestrictionFlags.BLOCK.value; // ブロック
flags = new sdkSymbol.models.AccountRestrictionFlags(f);
// アドレス制限設定Tx作成
descriptor = new sdkSymbol.descriptors.AccountAddressRestrictionTransactionV1Descriptor( // Txタイプ:アドレス制限設定Tx
flags, // アドレス制限フラグ
[ // 設定アドレス
bob.address,
],
[] // 解除アドレス
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名
sig = carol.signTransaction(tx);
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
// アドレス制限設定Txをアナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
このコードは、Symbolブロックチェーンにおいて「特定のネットワーク参加者(アカウント)からの連絡や送金を遮断する設定」を行うためのプログラムです。
簡単に言うと、Carol(署名者)がBob(対象アドレス)をブラックリスト(着信拒否)に登録している状態を作り出しています。
コードの主な流れ
- Bobのアカウント作成: sdkCore.PrivateKey.random() を使って、制限対象となるBobのアカウントを一時的に生成しています。
- 制限フラグの設定: ADDRESS: アドレスベースの制限であることを指定。 BLOCK: 指定したアドレスを「ブロック(拒否)」することを指定。
- トランザクション(Tx)の作成: AccountAddressRestrictionTransactionV1Descriptor を使い、「Bobのアドレスからのアクセスをブロックする」という設定を定義しています。 このTxの署名者は Carol (carol.publicKey) です。つまり、Carolのアカウントに対して制限ルールを適用しようとしています。
- 署名と送信: Carolがこの設定に同意する署名を行い、ネットワーク(ノード)へ送信(アナウンス)しています。
指定モザイクの受信制限
// 制限設定
f = sdkSymbol.models.AccountRestrictionFlags.MOSAIC_ID.value; // モザイク制限
f += sdkSymbol.models.AccountRestrictionFlags.BLOCK.value; // ブロック
flags = new sdkSymbol.models.AccountRestrictionFlags(f);
// モザイク制限設定Tx作成
descriptor = new sdkSymbol.descriptors.AccountMosaicRestrictionTransactionV1Descriptor( // Txタイプ:モザイク制限設定Tx
flags, // モザイク制限フラグ
[ // 設定モザイク
new sdkSymbol.models.UnresolvedMosaicId(0x72C0212E67A08BCEn),
],
[] // 解除モザイク
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名
sig = carol.signTransaction(tx);
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
// モザイク制限設定Txをアナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
このコードは、Symbolブロックチェーンにおいて「特定種類のトークン(モザイク)を受け取らないように設定する」ためのプログラムです。今回は「特定のモノ(モザイク)」を拒否する設定になっています。
コードの主な意味
- 制限フラグの設定 (MOSAIC_ID + BLOCK): MOSAIC_ID: 「モザイク(トークン)」を対象にした制限であることを示します。 BLOCK: 指定したモザイクを「ブロック(拒否)」することを示します。 つまり、「このトークンが入ってくるのを拒否する」というブラックリスト設定です。
- 対象モザイクの指定: 0x72C0212E67A08BCEn というIDを持つモザイクを制限対象としています。
- トランザクションの作成: AccountMosaicRestrictionTransactionV1Descriptor を使用し、署名者である Carol のアカウントに対して、「このモザイクを含む送金を受け付けない」というルールを適用しようとしています。
指定トランザクションの送信制限
// 制限設定
f = sdkSymbol.models.AccountRestrictionFlags.TRANSACTION_TYPE.value; // トランザクション制限
f += sdkSymbol.models.AccountRestrictionFlags.OUTGOING.value; // 送信
flags = new sdkSymbol.models.AccountRestrictionFlags(f);
// トランザクション制限設定Tx作成
descriptor = new sdkSymbol.descriptors.AccountOperationRestrictionTransactionV1Descriptor( // Txタイプ:トランザクション制限設定Tx
flags, // トランザクション制限フラグ
[ // 設定トランザクション
sdkSymbol.models.TransactionType.ACCOUNT_OPERATION_RESTRICTION.value,
],
[] // 解除トランザクション
);
tx = facade.createTransactionFromTypedDescriptor(
descriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名
sig = carol.signTransaction(tx);
jsonPayload = facade.transactionFactory.static.attachSignature(tx, sig);
// トランザクション制限設定Txをアナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
今回のコードは、「自分のアカウントができる操作(送信)を制限する」という、セキュリティ強化やガバナンスのための設定です。特にこのコードは、「アカウント制限の設定変更そのものを禁止する」という、非常に強力な(そして少し注意が必要な)操作を行っています。
コードの主な意味
- 制限フラグの設定 (TRANSACTION_TYPE + OUTGOING): TRANSACTION_TYPE: 特定の「操作(トランザクションの種類)」を対象にします。 OUTGOING: 自分から発信(送信)する操作を制限します(これまでは受信側の制限でした)。 BLOCK: デフォルトの挙動として、指定した操作を「禁止(ブロック)」します。
- 対象トランザクションの指定: ACCOUNT_OPERATION_RESTRICTION: 「アカウント制限設定(このコードそのもののTxタイプ)」を対象に指定しています。
- トランザクションの作成: 署名者 Carol のアカウントから、今後「アカウント制限を設定・変更するトランザクション」を送信できないようにします。
このコードが実行されるとどうなるか?
この設定が承認されると、Carolのアカウントは「二度と(この方法では)アカウント制限のルールを変更できなくなり」ます。
11.2 グローバルモザイク制限
// モザイクフラグ設定
f = sdkSymbol.models.MosaicFlags.NONE.value;
f += sdkSymbol.models.MosaicFlags.SUPPLY_MUTABLE.value; // 供給量変更の可否
f += sdkSymbol.models.MosaicFlags.TRANSFERABLE.value; // 第三者への譲渡可否
f += sdkSymbol.models.MosaicFlags.RESTRICTABLE.value; // グローバル制限設定の可否
f += sdkSymbol.models.MosaicFlags.REVOKABLE.value; // 発行者からの還収可否
flags = new sdkSymbol.models.MosaicFlags(f);
// ナンス設定
array = new Uint8Array(sdkSymbol.models.MosaicNonce.SIZE);
crypto.getRandomValues(array);
nonce = sdkSymbol.models.MosaicNonce.deserialize(array);
// モザイク定義
mosaicDefDescriptor = new sdkSymbol.descriptors.MosaicDefinitionTransactionV1Descriptor( // Txタイプ:モザイク定義Tx
new sdkSymbol.models.MosaicId(sdkSymbol.generateMosaicId(carol.address, nonce)),
new sdkSymbol.models.BlockDuration(0n), // duration:有効期限
nonce, // ナンス
flags, // モザイクフラグ
0 // divisibility:可分性
);
mosaicDefTx = facade.createEmbeddedTransactionFromTypedDescriptor(
mosaicDefDescriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
);
// モザイク変更
mosaicChangeDescriptor = new sdkSymbol.descriptors.MosaicSupplyChangeTransactionV1Descriptor( // Txタイプ:モザイク変更Tx
new sdkSymbol.models.UnresolvedMosaicId(mosaicDefTx.id.value),
new sdkSymbol.models.Amount(1000000n), // 数量
sdkSymbol.models.MosaicSupplyChangeAction.INCREASE // アクション
);
mosaicChangeTx = facade.createEmbeddedTransactionFromTypedDescriptor(
mosaicChangeDescriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
);
// キー生成(v2 準拠)
// メタデータキー生成と同じロジックのため、metadataGenerateKey()を利用する
key = sdkSymbol.metadataGenerateKey("KYC"); // restrictionKey
// グローバルモザイク制限
mosaicGlobalResDescriptor = new sdkSymbol.descriptors.MosaicGlobalRestrictionTransactionV1Descriptor( // Txタイプ:グローバルモザイク制限Tx
new sdkSymbol.models.UnresolvedMosaicId(mosaicDefTx.id.value), // 制限をかけるモザイクID
new sdkSymbol.models.UnresolvedMosaicId(0n), // 参照するモザイクID。制限をかけるモザイクIDと一致する場合は 0 を指定する
key, // restriction key
0n, // 現在の restriction value
1n, // 新しく設定する restriction value
sdkSymbol.models.MosaicRestrictionType.NONE, // 現在の restriction type
sdkSymbol.models.MosaicRestrictionType.EQ // 新しく設定する restriction type
);
mosaicGlobalResTx = facade.createEmbeddedTransactionFromTypedDescriptor(
mosaicGlobalResDescriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
);
// 更新する場合は以下も設定する必要あり
// - mosaicGlobalResTx.previousRestrictionValue
// - mosaicGlobalResTx.previousRestrictionType
embeddedTransactions = [
mosaicDefTx,
mosaicChangeTx,
mosaicGlobalResTx
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
0 // 連署者数
);
// 署名とアナウンス
sig = carol.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: jsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
今回のコードは、「発行するトークン(モザイク)そのものにルールを組み込む」という非常に高度な操作を行っています。
一言でいうと、「KYC(本人確認)などの条件を満たした人だけが扱える、特別な制限付きトークンを新規発行する」という処理です。
1. 3つの処理を1つにまとめた「アグリゲートTx」
このコードは、3つの異なるアクションを「アグリゲート・コンプリート・トランザクション」としてまとめ、すべて成功するか、すべて失敗するか」のオール・オア・ナッシングで実行しています。
① モザイクの定義 (MosaicDefinition)
新しいトークンを作成します。注目すべきは設定されたフラグです:
- SUPPLY_MUTABLE: あとで枚数を増やせる。
- TRANSFERABLE: 第三者に送れる(ただし、後述の制限に従う)。
- RESTRICTABLE: 今回の肝。 このモザイクに制限ルールを適用できるようにします。
- REVOKABLE: 発行者が後から回収できる。
② 供給量の設定 (MosaicSupplyChange)
作成したモザイクを、とりあえず「1,000,000個(最小単位)」発行して発行者の手元に用意します。
③ グローバル制限の設定 (MosaicGlobalRestriction)
ここが最も重要な部分です。
- キー: "KYC" という名前の制限ルールを作成。
- 条件: EQ 1(1と等しいこと)。
- 意味: 「このモザイクを保有・送金するには、そのアカウントの『KYC』という属性値が『1』に設定されていなければならない」というネットワーク全体のルールを定義します。
2. このコードが実行されるとどうなるか?
このコードが承認された直後の状態は以下の通りです。
- 新しいモザイクが誕生し、Carolが1,000,000個持っている。
- しかし、まだ誰も(Carol自身ですら)このモザイクを誰かに送ることはできない。
- なぜなら、まだ誰の「KYC」フラグも「1」になっていないからです。
アカウントへのモザイク制限適用
// Carolに適用
carolMosaicAddressResDescriptor = new sdkSymbol.descriptors.MosaicAddressRestrictionTransactionV1Descriptor( // Txタイプ:モザイク制限適用Tx
new sdkSymbol.models.UnresolvedMosaicId(mosaicDefTx.id.value), // 適用するモザイクID
key, // restriction key
0xFFFFFFFFFFFFFFFFn, // 現在の restriction value (制限がかけられていない場合は 0xFFFFFFFFFFFFFFFF)
1n, // 新しく設定する restriction value
carol.address
);
carolMosaicAddressResTx = facade.createTransactionFromTypedDescriptor(
carolMosaicAddressResDescriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名とアナウンス
carolSig = carol.signTransaction(carolMosaicAddressResTx);
carolJsonPayload = facade.transactionFactory.static.attachSignature(carolMosaicAddressResTx, carolSig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: carolJsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
bob = facade.createAccount(sdkCore.PrivateKey.random());
// Bobに適用
bobMosaicAddressResDescriptor = new sdkSymbol.descriptors.MosaicAddressRestrictionTransactionV1Descriptor( // Txタイプ:モザイク制限適用Tx
new sdkSymbol.models.UnresolvedMosaicId(mosaicDefTx.id.value), // 適用するモザイクID
key, // restriction key
0xFFFFFFFFFFFFFFFFn, // 現在の restriction value (制限がかけられていない場合は 0xFFFFFFFFFFFFFFFF)
1n, // 新しく設定する restriction value
bob.address
);
bobMosaicAddressResTx = facade.createTransactionFromTypedDescriptor(
bobMosaicAddressResDescriptor, // トランザクション Descriptor 設定
carol.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2 // Deadline:有効期限(秒単位)
);
// 署名とアナウンス
bobSig = carol.signTransaction(bobMosaicAddressResTx);
bobJsonPayload = facade.transactionFactory.static.attachSignature(bobMosaicAddressResTx, bobSig);
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: bobJsonPayload,
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
コードの主な意味
1. Carolへの適用 (carolMosaicAddressResTx)
発行者であるCarol自身に対して、key(KYC)の値を 1n に設定しています。
- 0xFFFFFFFFFFFFFFFFn: これは「まだこのモザイクに関する制限値を持っていない(初期状態)」ことを示す特別な値です。
2. Bobへの適用 (bobMosaicAddressResTx)
新しく生成したBobのアカウントに対しても、同様にKYCの値を 1n に設定しています。
- ここで重要なのは、署名者が carol であることです。このモザイクの制限をコントロールできるのは発行者(Carol)だけなので、Carolが「Bobは許可されたユーザーである」と承認する形になります。
このコードが実行されるとどうなるか?
この2つのトランザクションが承認されることで、ようやく以下のことが可能になります:
- 送金が可能になる: Carol(属性1)からBob(属性1)へ、あの制限付きモザイクを送金できるようになります。
- ルールの合致: ネットワークが送金をチェックする際、「送り手も受け手も条件(KYC=1)を満たしているな」と判断してパスさせます。
12.オフライン署名
12.1 トランザクション作成
bob = facade.createAccount(sdkCore.PrivateKey.random());
// アグリゲートTxに含めるTxを作成
innerTxDescriptor1 = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
bob.address, // Bobへの送信
[],
'\0tx1' // メッセージ
);
innerTx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
innerTxDescriptor1, // トランザクション Descriptor 設定
alice.publicKey, // Aliceから
);
innerTxDescriptor2 = new sdkSymbol.descriptors.TransferTransactionV1Descriptor( // Txタイプ:転送Tx
alice.address, // Aliceへの送信
[],
'\0tx2' // メッセージ
);
innerTx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
innerTxDescriptor2, // トランザクション Descriptor 設定
bob.publicKey, // Bobから
);
embeddedTransactions = [
innerTx1,
innerTx2
];
// アグリゲートTx作成
aggregateDescriptor = new sdkSymbol.descriptors.AggregateCompleteTransactionV3Descriptor(
facade.static.hashEmbeddedTransactions(embeddedTransactions),
embeddedTransactions
);
aggregateTx = facade.createTransactionFromTypedDescriptor(
aggregateDescriptor, // トランザクション Descriptor 設定
alice.publicKey, // 署名者公開鍵
100, // 手数料乗数
60 * 60 * 2, // Deadline:有効期限(秒単位)
1 // 連署者数
);
// 署名
sig = alice.signTransaction(aggregateTx);
jsonPayload = facade.transactionFactory.static.attachSignature(aggregateTx, sig);
signedHash = facade.hashTransaction(aggregateTx).toString();
signedPayload = JSON.parse(jsonPayload).payload;
console.log(signedPayload);
今回のコードは、Symbolブロックチェーンの真骨頂とも言える「アグリゲート・トランザクション(複合トランザクション)の構築プロセスです。
一言でいうと、「AliceとBobの間で、複数の送金を『同時に、かつ不可分(すべて成功か失敗か)』に実行するための準備」をしています。
1. コードの主な構造
このコードは「アグリゲート・コンプリート」という形式を目指しており、以下の2つの内部トランザクション(インナートランザクション)を1つのパッケージにまとめています。
- インナーTx 1: AliceからBobへメッセージ「tx1」を送信。
- インナーTx 2: BobからAliceへメッセージ「tx2」を送信。
**ここがポイント:**この2つの操作をアグリゲートTxに包むことで、「Aliceだけ送ってBobが送らない」という不公平が起きないよう、「両方の合意(署名)が揃った瞬間に、両方の送金が同時に実行される」仕組みを作っています。
2. コードの詳細解説
- createEmbeddedTransactionFromTypedDescriptor: 単体のTxではなく、アグリゲートTxの中に「埋め込むためのTx」として作成しています。
- AggregateCompleteTransactionV3Descriptor: 「コンプリート(完全)」タイプを選択しています。これは、ネットワークにアナウンスする時点で、必要な全員の署名が揃っている(または揃える)必要がある形式です。
- facade.static.hashEmbeddedTransactions: 中身のインナーTxが改ざんされないよう、それらのハッシュ値を計算して親のTxに記録します。
- 連署者数 1: 発信者(Alice)以外に、あと1人(Bob)の署名が必要であることを明示しています。
3. この後の展開
現在のコードは console.log(signedPayload) で終わっていますが、このままノードに投げても失敗(または無視)されます。
なぜなら、「Bobの署名」がまだ足りないからです。
正常に完了させるためのステップ:
- Aliceが署名したペイロードをBobに渡す。
- Bobが内容を確認し、自分の秘密鍵で「連署(Cosignature)」を行う。
- Aliceの署名 + Bobの連署が揃った状態で、初めてノードへ PUT /transactions する。
まとめ
このコードは、「トラストレス(信頼不要)な交換」の基礎です。 例えば、「Aliceが100XYM送る代わりに、Bobがチケット(モザイク)を送る」といった交換を、第三者を介さず安全に行うために使われます。
12.3 Aliceによるアナウンス
recreatedTx = facade.transactionFactory.static.deserialize(sdkCore.utils.hexToUint8(JSON.parse(jsonPayload).payload));
// 連署者の署名を追加
cosignature = new sdkSymbol.models.Cosignature();
signTxHash = facade.hashTransaction(aggregateTx);
cosignature.parentHash = signTxHash;
cosignature.version = 0n;
cosignature.signerPublicKey = bobSignedTxSignerPublicKey;
cosignature.signature = bobSignedTxSignature;
recreatedTx.cosignatures.push(cosignature);
signedPayload = sdkCore.utils.uint8ToHex(recreatedTx.serialize());
// アナウンス
await fetch(
new URL('/transactions', NODE),
{
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({"payload": signedPayload}),
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
このコードは、前回作成した「AliceとBobの共同作業(アグリゲートTx)」を完成させるための「最後のピース(Bobの署名)」をはめ込み、ネットワークに送信する処理です。
一言でいうと、「Bobが同意のハンコを押し、取引が成立した書類を役所(ブロックチェーン)に提出した」という状態を指します。
コードの主な流れ
- Txの復元 (deserialize): Aliceが作成した未完成のトランザクション(JSONデータ)を、プログラムが操作しやすいオブジェクト形式に戻しています。
- 連署(Cosignature)の作成: ここがこのコードの核心です。Bobの署名データを「連署(Cosignature)」という専用のパーツとして組み立てています。 parentHash: どの取引に対して署名するかを特定するハッシュ値。 signerPublicKey: Bobの公開鍵。 signature: Bobの秘密鍵で生成された署名データ。
- 署名の合体 (push): 復元したトランザクションの cosignatures というリストに、Bobの署名を追加しています。これで Aliceの署名 + Bobの署名が揃いました。
- シリアライズとアナウンス: 完成したデータを再び16進数(Hex)形式に変換し、APIを通じてSymbolノードへ送信しています。
このコードが成功するとどうなるか?
このリクエストが承認されると、アグリゲートTxの中に含まれていた2つの送金が同時に、一瞬で実行されます。
- Alice → Bob へのメッセージ送信が成功する。
- Bob → Alice へのメッセージ送信が成功する。
もしBobの署名が偽物だったり、不備があったりした場合は、このアグリゲートTx全体が拒否されます。これにより「Aliceだけが損をする」といった事態が防がれています。
第13章 検証
13.1 トランザクションの検証
検証するペイロード
payload = '2802000000000000A5151FD55D82351DD488DB5563DD328DA72B2AD25B513C1D0F7F78AFF4D35BA094ABF505C74E6D6BE1FA19F3E5AC60A85E1A4EDC4AC07DECC0E56C59D5D24F0B69A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB0000000002984141A0D70000000000000EEAD6810500000062E78B6170628861B4FC4FCA75210352ACDBD2378AC0A447A3DCF63F969366BB1801000000000000540000000000000069A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB000000000198544198A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329D04000000000000000074783100000000590000000000000069A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB000000000198444198A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329D6668A0DE72812AAE05000500746573743100000000000000590000000000000069A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB000000000198444198A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329DBF85DADBFD54C48D050005007465737432000000000000000000000000000000662CEDF69962B1E0F1BF0C43A510DFB12190128B90F7FE9BA48B1249E8E10DBEEDD3B8A0555B4237505E3E0822B74BCBED8AA3663022413AFDA265BE1C55431ACAE3EA975AF6FD61DEFFA6A16CBA5174A16EF5553AE669D5803A0FA9D1424600';
height = 686312;
ペイロードとは、その取引(トランザクション)で運ばれる実際のメッセージや取引データを言います トラックに例えると、車両情報や運転手の免許証、配送ルートなどが「ヘッダー(制御情報)」で、荷台に載せている実際の荷物が「ペイロード」にあたります。 ブロックチェーンSymbolにおいてトランザクションには ・手数料 ・署名 ・送信先 ・メッセージ が含まれています。
この 「メッセージ部分」や「Txに埋め込まれたデータ」を ペイロードと呼ぶことが多いです。
マークルコンポーネントハッシュの計算
マークルコンポーネントハッシュ(Merkle Component Hash)は、 「トランザクションを構成する各要素(コンポーネント)をハッシュ化して、マークル化したもの」です。
ここでマークル(Merkle)とは、データを ハッシュにし、それを 木構造(Merkle Tree)にして最終的に 1つのハッシュ(Merkle Root)にまとめる仕組みです。 コンポーネントとは トランザクションを構成する部品のことであり、Symbolのトランザクションなら例として:
- 署名
- 送信元
- 受信先
- 金額
- モザイク
- メッセージ(payload)
- 期限
- ネットワークタイプ
- 手数料 など
👉 これら 1つ1つが「コンポーネント」
つまりマークルコンポーネントハッシュとは
各コンポーネントを個別にハッシュ化し、それらをマークルツリーとしてまとめたハッシュ
[component1] → hash1
[component2] → hash2
[component3] → hash3
↓
Merkle Tree
↓
Merkle Component Hash
merkleComponentHash = hash;
if (tx.cosignatures !== undefined && tx.cosignatures.length > 0) {
hasher = sha3_256.create();
hasher.update(hash.bytes);
for (cosignature of tx.cosignatures){
hasher.update(cosignature.signerPublicKey.bytes);
}
merkleComponentHash = sdkCore.utils.uint8ToHex(hasher.digest());
}
console.log(merkleComponentHash);
このコードは、Symbolにおける「トランザクションの完全な識別子(Merkle Component Hash)」を算出するロジックを表しています。
一言でいうと、「取引内容(ハッシュ)」に「誰が同意したか(連署人の公開鍵)」を混ぜ込んで、その取引固有の最終的なハッシュ値を作る処理です。
コードの解説
この処理は、特にアグリゲートトランザクション(複合取引)において重要になります。
- 初期値のセット (merkleComponentHash = hash; ) :まず、ベースとなるトランザクションハッシュ(内容のハッシュ)を代入します。
- 署人(Cosignatures)がいるかどうかの確認 : if (tx.cosignatures !== undefined && tx.cosignatures.length > 0) 自分以外の署名(連署)が必要な取引かどうかをチェックします。
- ハッシュの再計算: 連署人がいる場合、SHA3-256で新しいハッシュを作ります。 まず、元のhashを混ぜる。 次に、すべての連署人の公開鍵(signerPublicKey)*を順番に混ぜる。 hasher.digest() でこれらを一つのハッシュにまとめます。
- 結果の出力:最終的に生成されたものが、そのトランザクションの正当性を証明するための Merkle Component Hash となります。
InBlockの検証
InBlock(インブロック)は、「そのトランザクションがブロックに取り込まれた状態」を指します。
状態の流れで見るとトランザクションは、だいたいこの順で進みます👇
- Unconfirmed
ノードに受理された まだブロックには入っていない ロールバックされる可能性あり - InBlock
ブロックに含まれた **まだ最終確定ではない フォークが起きると消える可能性はゼロではない - Confirmed
十分な承認数(深さ)を得て確定 **通常はもう覆らない
//トランザクションから計算
leaf = new sdkCore.Hash256(merkleComponentHash);
//ノードから取得
HRoot = await fetch(
new URL('/blocks/' + height, NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return new sdkCore.Hash256(json.block.transactionsHash);
});
merkleProof = await fetch(
new URL('/blocks/' + height + '/transactions/' + leaf + '/merkle', NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
let paths = [];
json.merklePath.forEach(path => paths.push({
"hash": new sdkCore.Hash256(path.hash),
"isLeft": path.position === "left"
}));
return paths;
});
result = sdkSymbol.proveMerkle(leaf, merkleProof, HRoot);
console.log(result);
コードの3つのステップ
このプログラムは、以下の3つのステップで「証拠」を集め、最後に照合しています。
1. 自分の手元にある「葉(Leaf)」を特定
leaf = new sdkCore.Hash256(merkleComponentHash);
前の質問で計算した「マークルコンポーネントハッシュ」を、検証したい取引のデータ(ツリーの末端=葉)として準備します。
2. ネットワーク(ノード)から「正解のルート」と「ルートへの道筋」を取得
- HRoot (transactionsHash):
ブロックヘッダーに記録されている、そのブロック内の全取引を要約した最終的なハッシュ値(ツリーの頂点=根)を取得します。 - merkleProof (merklePath):
自分の取引(葉)から頂点(根)にたどり着くために必要な、他の枝のハッシュ値のリストを取得します。
3. 数学的な照合実行
result = sdkSymbol.proveMerkle(leaf, merkleProof, HRoot);
sdkSymbol.proveMerkle 関数の中で以下の計算を行っています。
- 「自分の取引(leaf)」と「もらった道筋(merkleProof)」を順番にハッシュ計算して合体させていく。
- 最終的に算出された値が、ノードから得た「正解のルート(HRoot)」と完全に一致するかを確認する。
なぜこれが必要なのか?(メリット)
もし result が true であれば、以下のことが100%保証されます。
- その取引は、間違いなく指定したブロック(height)に記録されている。
- ノードが嘘をついて、存在しない取引を「ある」と言い張ることはできない。
- 取引内容が1ビットでも改ざんされていれば、ハッシュ計算の結果がルートと一致しなくなるため、不正を見抜ける。
[!TIP] 「信じるな、検証せよ(Don't Trust, Verify)」 というブロックチェーンの理念を体現したコードです。ノードが返してきた情報を鵜呑みにせず、自分の手元で計算して正しさを裏付けています。
13.2 ブロックヘッダーの検証
normalブロックの検証
blockInfo = await fetch(
new URL('/blocks/' + height, NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
block = blockInfo.block;
previousBlockHash = await fetch(
new URL('/blocks/' + (height - 1), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json.meta.hash;
});
if(block.type === sdkSymbol.models.BlockType.NORMAL.value){
hasher = sha3_256.create();
hasher.update(Buffer.from(block.signature,'hex')); //signature
hasher.update(Buffer.from(block.signerPublicKey,'hex')); //publicKey
hasher.update(sdkCore.utils.intToBytes(block.version, 1));
hasher.update(sdkCore.utils.intToBytes(block.network, 1));
hasher.update(sdkCore.utils.intToBytes(block.type, 2));
hasher.update(sdkCore.utils.intToBytes(BigInt(block.height), 8));
hasher.update(sdkCore.utils.intToBytes(BigInt(block.timestamp), 8));
hasher.update(sdkCore.utils.intToBytes(BigInt(block.difficulty), 8));
hasher.update(Buffer.from(block.proofGamma,'hex'));
hasher.update(Buffer.from(block.proofVerificationHash,'hex'));
hasher.update(Buffer.from(block.proofScalar,'hex'));
hasher.update(Buffer.from(previousBlockHash,'hex'));
hasher.update(Buffer.from(block.transactionsHash,'hex'));
hasher.update(Buffer.from(block.receiptsHash,'hex'));
hasher.update(Buffer.from(block.stateHash,'hex'));
hasher.update(Buffer.from(block.beneficiaryAddress,'hex'));
hasher.update(sdkCore.utils.intToBytes(block.feeMultiplier, 4));
hash = sdkCore.utils.uint8ToHex(hasher.digest());
console.log(hash === blockInfo.meta.hash);
}
コードの主要なステップ
1. データの収集
検証したいブロック(height)の情報と、その1つ前のブロック(height - 1)のハッシュ値を取得しています。
- ブロックチェーンは「前のブロックのハッシュ」を自分の中に含めることで鎖のようにつながるため、この previousBlockHash は検証に不可欠です。
2. 各フィールドのシリアライズ(直列化)
hasher.update(...) を繰り返している部分は、ブロックヘッダーに含まれる各要素(署名、公開鍵、高さ、タイムスタンプ、各マークルルートなど)を、特定の順番でバイナリデータ(バイト列)に変換してハッシュ関数に投入しています。
3. 計算結果の照合
hash = sdkCore.utils.uint8ToHex(hasher.digest());
console.log(hash === blockInfo.meta.hash);
手元のデータから算出したハッシュ値(hash)が、ノードから送られてきた「このブロックのハッシュ値です」という公称値(blockInfo.meta.hash)と一致するかを確認しています。
検証している主要な項目
このハッシュ計算には、以下のような重要なデータが含まれています。
- transactionsHash: そのブロックに含まれる全取引のまとめ(前回解説したマークルルート)。
- previousBlockHash: 前のブロックとのつながり。
- stateHash: そのブロック完了時点での全アカウントの残高などの状態。
- receiptsHash: 手数料の支払いやアーベスティング報酬などの領収書データ。
stateHashの検証
SymbolにおけるstateHash(ステートハッシュ)とは、そのブロックが承認された時点での「ネットワーク全体の全アカウントの状態(ステート)」を要約したハッシュ値のことです。
ブロックチェーンを「通帳」に例えるなら、トランザクションは「入出金の履歴」であり、ステートハッシュは「その時点での全員の最終残高一覧表」をギュッと凝縮したスタンプのようなものです。
ステートハッシュが記録している「状態」とは?
Symbolのステートハッシュには、主に以下の情報が反映されています。
- アカウント情報: 各アドレスの残高(モザイク保有量)、公開鍵、メタデータ。
- ネームスペース: 登録されているドメイン名の所有権や有効期限。
- モザイク定義: 発行されているトークンの規約や発行量。
- マルチシグ構成: どのアカウントがどのマルチシグに属しているか。
なぜステートハッシュが必要なのか?
- データの完全性の保証
もし誰かが「自分の残高を勝手に100万XYM増やそう」としてデータベースを改ざんしても、その瞬間に計算されるステートハッシュがブロックに記録された正解のハッシュ値と一致しなくなるため、不正が即座にバレます。 - ライトクライアント(軽量ノード)の検証
スマホアプリなどの「全履歴を保持していないアプリ」でも、このハッシュと「マークルパトリシアツリーの証明データ」を使うことで、**「自分の今の残高は本当に正しいのか?」**を全データをダウンロードせずに数学的に確認できます。 - 高速な同期
新しいノードがネットワークに参加する際、過去の全履歴を一つずつ計算し直さなくても、信頼できる最新のステートハッシュを基準にして現在の状態を同期することが可能になります。
ブロックヘッダー内での位置づけ
前回のコードで、hasher.update(Buffer.from(block.stateHash,'hex')); という一文があった通り、ステートハッシュはブロックヘッダーを構成する重要なパズルの一片です。
- transactionsHash: そのブロックで「何が起きたか」を保証。
- stateHash: その結果「どうなったか」を保証。
この2つがセットになることで、正しい処理が行われ、その結果として正しい残高に更新された」ことが完璧に証明される仕組みになっています。
13.3 アカウント・メタデータの検証
検証用共通関数
//葉のハッシュ値取得関数
function getLeafHash(encodedPath, leafValue){
const hasher = sha3_256.create();
return sdkCore.utils.uint8ToHex(hasher.update(sdkCore.utils.hexToUint8(encodedPath + leafValue)).digest());
}
//枝のハッシュ値取得関数
function getBranchHash(encodedPath, links){
const branchLinks = Array(16).fill(sdkCore.utils.uint8ToHex(new Uint8Array(32)));
links.forEach((link) => {
branchLinks[parseInt(`0x${link.bit}`, 16)] = link.link;
});
const hasher = sha3_256.create();
const bHash = sdkCore.utils.uint8ToHex(hasher.update(sdkCore.utils.hexToUint8(encodedPath + branchLinks.join(''))).digest());
return bHash;
}
//ワールドステートの検証
function checkState(stateProof,stateHash,pathHash,rootHash){
merkleLeaf = undefined;
merkleBranches = [];
stateProof.tree.forEach(n => {
if (n.type === 255) {
merkleLeaf = n;
} else {
merkleBranches.push(n);
}
});
merkleBranches.reverse();
const leafHash = getLeafHash(merkleLeaf.encodedPath,stateHash);
let linkHash = leafHash; //最初のlinkHashはleafHash
let bit="";
for(let i = 0; i < merkleBranches.length; i++){
const branch = merkleBranches[i];
const branchLink = branch.links.find(x=>x.link === linkHash)
linkHash = getBranchHash(branch.encodedPath,branch.links);
bit = merkleBranches[i].path.slice(0,merkleBranches[i].nibbleCount) + branchLink.bit + bit ;
}
const treeRootHash = linkHash; //最後のlinkHashはrootHash
let treePathHash = bit + merkleLeaf.path;
if(treePathHash.length % 2 == 1){
treePathHash = treePathHash.slice( 0, -1 );
}
//検証
console.log(treeRootHash === rootHash);
console.log(treePathHash === pathHash);
}
コードの主要な構成要素
Symbolのステート(状態)は、16進数のキー(Path)によって管理される巨大なツリー構造になっています。
1. 葉(Leaf)と枝(Branch)のハッシュ計算
- getLeafHash: ツリーの末端にあるデータ(アカウント情報など)のハッシュを作ります。
- getBranchHash: 16本の道(0〜Fの16進数)を持つ「交差点」のハッシュを作ります。Symbol(およびEthereumなど)のステートツリーは、16進数の1桁ごとに分岐するため、枝は16個のリンクを保持します。
2. checkState 関数のロジック
function checkState(stateProof,stateHash,pathHash,rootHash)
引数の意味
- stateProof:ノードが返した証明
- stateHash:対象 state の値
- pathHash:キーのハッシュ
- rootHash:ブロックの World State Root
この関数は、ノードから提供された「証明データ(stateProof)」を使い、以下の手順で逆算(ボトムアップ計算)を行います。
- データの整理: 証明データの中から「末端のデータ(Leaf)」と、そこに至るまでの「交差点(Branches)」を切り分けます。
- 下から上へ計算: まず「葉のハッシュ」を計算。 次に、その葉が含まれる「枝」を見つけ、その枝全体のハッシュを計算。 これをルート(根)に到達するまで繰り返します。
- パス(Path)の復元: 枝を辿る際、どの分岐(bit)を通ったかを繋ぎ合わせることで、そのデータが保存されている「住所(PathHash)」を復元します。
checkState関数の詳細
Leaf と Branch を分離
stateProof.tree.forEach(n => {
if (n.type === 255) {
merkleLeaf = n;
} else {
merkleBranches.push(n);
}
});
merkleBranches.reverse();
- type === 255 → Leaf
- それ以外 → Branch
- Leaf から Root に向かう順に並べ替え
Leaf Hash を計算
const leafHash = getLeafHash(merkleLeaf.encodedPath,stateHash);
👉 ステート値から 最初の linkHash を作る
Branch を辿りながら Root を再構築
let linkHash = leafHash;
let bit = "";
for (...) {
const branchLink = branch.links.find(x => x.link === linkHash);
linkHash = getBranchHash(branch.encodedPath,branch.links);
bit = branch.path.slice(0,branch.nibbleCount) + branchLink.bit + bit;
}
自分が来た方向(branchLink.bit)を記録
- 上に登りながらハッシュを更新 パスを復元
👉 「この Leaf がどのパスに存在するか」*再構築
Root 到達
const treeRootHash = linkHash;
👉 これが 計算上の World State Root
PathHash 再構築
let treePathHash = bit + merkleLeaf.path;
if(treePathHash.length % 2 == 1){
treePathHash = treePathHash.slice(0, -1);
}
nibble 単位 → byte 境界に調整
👉 完全な pathHash を再現
検証
console.log(treeRootHash === rootHash);
console.log(treePathHash === pathHash);
console.log で出力している内容は以下の通りです。
- treeRootHash === rootHash: 計算して導き出した「根」が、ブロックヘッダーに記録されている公式な stateHash と一致するか。一致すれば、中身のデータが正しい証拠です。
- treePathHash === pathHash: 計算して復元した「住所」が、本来のアドレス等から導かれるハッシュ値と一致するか。一致すれば、正しい場所に格納されているデータである証拠です。
この処理の凄さ(信頼の最小化)
このコードが実行できると、あなたはノードを完全に疑いながらも、正しい情報を得ることができます。
- ノードが「あなたの残高は0です」と嘘のデータを送ってきたとしても、この検証プロセスを通すと treeRootHash がブロックヘッダーの公式値と一致しなくなるため、嘘を即座に見抜けます。
- これは「ライトクライアント(軽量ウォレット)」が、信頼できない公共ノードに接続する際に自身の安全を守るための最強の盾となります。
13.3.1 アカウント情報の検証
aliceAddress = new sdkSymbol.Address("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ");
hasher = sha3_256.create();
alicePathHash = sdkCore.utils.uint8ToHex(hasher.update(aliceAddress.bytes).digest());
hasher = sha3_256.create();
aliceInfo = await fetch(
new URL('/accounts/' + aliceAddress.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json.account;
});
// アカウント情報から StateHash を導出
// catbuffer-typescript が使える場合はそちらを利用すると楽
format = (parseInt(aliceInfo.importance) === 0 || aliceInfo.activityBuckets.length < 5 ? 0x00 : 0x01);
supplementalPublicKeysMask = 0x00;
linkedPublicKey = new Uint8Array([]);
if (aliceInfo.supplementalPublicKeys.linked !== undefined) {
supplementalPublicKeysMask |= 0x01;
linkedPublicKey = sdkCore.utils.hexToUint8(aliceInfo.supplementalPublicKeys.linked.publicKey);
}
nodePublicKey = new Uint8Array([]);
if (aliceInfo.supplementalPublicKeys.node !== undefined) {
supplementalPublicKeysMask |= 0x02;
nodePublicKey = sdkCore.utils.hexToUint8(aliceInfo.supplementalPublicKeys.node.publicKey);
}
vrfPublicKey = new Uint8Array([]);
if (aliceInfo.supplementalPublicKeys.vrf !== undefined) {
supplementalPublicKeysMask |= 0x04;
vrfPublicKey = sdkCore.utils.hexToUint8(aliceInfo.supplementalPublicKeys.vrf.publicKey);
}
votingPublicKeys = new Uint8Array([]);
if (aliceInfo.supplementalPublicKeys.voting !== undefined) {
aliceInfo.supplementalPublicKeys.voting.publicKeys.forEach(key => {
votingPublicKeys = new Uint8Array([...votingPublicKeys, ...sdkCore.utils.hexToUint8(key.publicKey)]);
});
}
importanceSnapshots = new Uint8Array([]);
if (parseInt(aliceInfo.importance) !== 0) {
importanceSnapshots = new Uint8Array([
...sdkCore.utils.intToBytes(BigInt(aliceInfo.importance), 8),
...sdkCore.utils.intToBytes(BigInt(aliceInfo.importanceHeight), 8),
]);
}
activityBuckets = new Uint8Array([]);
if (aliceInfo.importance > 0) {
for (idx = 0; idx < aliceInfo.activityBuckets.length || idx < 5; idx++) {
bucket = aliceInfo.activityBuckets[idx];
activityBuckets = new Uint8Array([
...activityBuckets,
...sdkCore.utils.intToBytes(BigInt(bucket.startHeight), 8),
...sdkCore.utils.intToBytes(BigInt(bucket.totalFeesPaid), 8),
...sdkCore.utils.intToBytes(bucket.beneficiaryCount, 4),
...sdkCore.utils.intToBytes(BigInt(bucket.rawScore), 8),
]);
}
}
balances = new Uint8Array([]);
if (aliceInfo.mosaics.length > 0) {
aliceInfo.mosaics.forEach(mosaic => {
balances = new Uint8Array([
...balances,
...sdkCore.utils.intToBytes(BigInt("0x" + mosaic.id), 8),
...sdkCore.utils.intToBytes(BigInt(mosaic.amount), 8),
]);
});
}
accountInfoBytes = new Uint8Array([
...sdkCore.utils.intToBytes(aliceInfo.version, 2),
...sdkCore.utils.hexToUint8(aliceInfo.address),
...sdkCore.utils.intToBytes(BigInt(aliceInfo.addressHeight), 8),
...sdkCore.utils.hexToUint8(aliceInfo.publicKey),
...sdkCore.utils.intToBytes(BigInt(aliceInfo.publicKeyHeight), 8),
...sdkCore.utils.intToBytes(aliceInfo.accountType, 1),
...sdkCore.utils.intToBytes(format, 1),
...sdkCore.utils.intToBytes(supplementalPublicKeysMask, 1),
...sdkCore.utils.intToBytes(votingPublicKeys, 1),
...linkedPublicKey,
...nodePublicKey,
...vrfPublicKey,
...votingPublicKeys,
...importanceSnapshots,
...activityBuckets,
...sdkCore.utils.intToBytes(aliceInfo.mosaics.length, 2),
...balances
]);
aliceStateHash = sdkCore.utils.uint8ToHex(hasher.update(accountInfoBytes).digest());
//サービス提供者以外のノードから最新のブロックヘッダー情報を取得
query = new URLSearchParams({
"order": "desc"
});
blockInfo = await fetch(
new URL('/blocks?' + query.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
rootHash = blockInfo.data[0].meta.stateHashSubCacheMerkleRoots[0];
//サービス提供者を含む任意のノードからマークル情報を取得
stateProof = await fetch(
new URL('/accounts/' + aliceAddress.toString() + '/merkle', NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
//検証
checkState(stateProof,aliceStateHash,alicePathHash,rootHash);
このコードは、これまで解説してきた「データのシリアライズ」「ハッシュ計算」「マークル証明」のすべてを組み合わせた、「Symbolにおけるアカウント情報の完全なセルフ検証」の実装コードです。
具体的には、「アリスというアカウントの残高や設定が、ネットワーク全体で合意された正解(StateHash)と一致するか」を、外部の情報を鵜呑みにせず自分の計算だけで証明しています。
コードの4つのステップ
1. アドレスのハッシュ化(アドレスの特定)
alicePathHash = sdkCore.utils.uint8ToHex(hasher.update(aliceAddress.bytes).digest());
Symbolのステートツリー内では、アカウントはアドレスそのものではなく、アドレスをハッシュ化した値(Path)の場所に格納されています。まずはその「住所」を計算しています。
2. アカウント情報のバイナリ変換(シリアライズ)
中盤の長い処理(accountInfoBytes を作っている部分)が最も泥臭く、かつ重要な部分です。 APIから取得した「アリスの情報(残高、公開鍵、インポータンスなど)」を、Symbolのプロトコルが定める厳格な順番でバイト列に変換しています。
- なぜやるか?: 1ビットでも順番やデータが違うとハッシュ値(aliceStateHash)が変わってしまうためです。これが「通帳の1行」を自分で書き起こす作業に相当します。
3. 根(Root)と証明パス(Proof)の取得
- rootHash: 最新ブロックのヘッダーから、ネットワーク全体の「正解の要約」を取得します。
- stateProof: ノードから「アリスのデータが根にどう繋がっているか」のルートマップを取得します。
4. 最終検証
checkState(stateProof, aliceStateHash, alicePathHash, rootHash);
最後に、前の質問で定義した checkState 関数を使い、「自分で計算したアリスのデータ」+「取得したルートマップ」=「公式の根(Root)」 になるかを検証します。
この一連の処理が保証すること
このコードを自分の端末で実行し、検証が成功すれば、以下のことが数学的に確実になります。
- 残高の真実性: アリスが持っているモザイク(XYMなど)の量は、ネットワークが認めた正しい量である。
- 改ざんの不在: 接続しているノードが、アリスの残高を勝手に書き換えて回答していない。
- 存在証明: アリスというアカウントが、指定された最新ブロックの時点で確かに存在している。
補足:なぜこんなに複雑なのか?
コード内で「catbuffer(キャットバッファ)」という言葉に触れられていますが、Symbolではデータの並び順が厳密に決まっています。 手動で intToBytes などを組み合わせて計算するのは大変なため、通常は自動生成されたライブラリを使いますが、このコードは「Symbolの裏側で何が起きているか」を型なしで生々しく実装した、非常に教育的な例と言えます。
13.3.2 モザイクへ登録したメタデータの検証
srcAddress = (new sdkSymbol.Address("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ")).bytes;
targetAddress = (new sdkSymbol.Address("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ")).bytes;
scopeKey = "CF217E116AA422E2"; // scopeKey, メタデータキーを指定
targetId = "1275B0B7511D9161"; // targetId, モザイクIDを指定
hasher = sha3_256.create();
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + scopeKey), 8)); // scopeKey, メタデータキーを指定
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + targetId), 8)); // targetId, モザイクIDを指定
hasher.update(Uint8Array.from([1])); // type: Mosaic 1
compositeHash = hasher.digest();
hasher = sha3_256.create();
hasher.update(compositeHash);
pathHash = sdkCore.utils.uint8ToHex(hasher.digest());
//stateHash(Value値)
hasher = sha3_256.create();
version = 1;
hasher.update(sdkCore.utils.intToBytes(version, 2)); //version
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + scopeKey), 8)); // scopeKey, メタデータキーを指定
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + targetId), 8)); // targetId, モザイクIDを指定
hasher.update(Uint8Array.from([1])); //mosaic
value = Buffer.from("test");
hasher.update(sdkCore.utils.intToBytes(value.length, 2));
hasher.update(value);
stateHash = sdkCore.utils.uint8ToHex(hasher.digest());
//サービス提供者以外のノードから最新のブロックヘッダー情報を取得
query = new URLSearchParams({
"order": "desc"
});
blockInfo = await fetch(
new URL('/blocks?' + query.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
rootHash = blockInfo.data[0].meta.stateHashSubCacheMerkleRoots[8];
//サービス提供者を含む任意のノードからマークル情報を取得
stateProof = await fetch(
new URL('/metadata/' + sdkCore.utils.uint8ToHex(compositeHash) + '/merkle', NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
//検証
checkState(stateProof,stateHash,pathHash,rootHash);
このコードは、Symbolの「モザイクメタデータ」の正当性を検証する処理です。
これまでの「アカウント情報」の検証と同じ仕組み(パトリシアメルクルツリー)を使っていますが、対象がメタデータになっている点が異なります。メタデータは、作成者・対象者・キー・対象IDの組み合わせで一意に決まるため、その「住所(Composite Hash)」の作り方が特徴的です。
コードのポイント解説
1. Composite Hash(複合ハッシュ)の生成
メタデータは単一のアドレスではなく、複数の要素の組み合わせで識別されます。
hasher.update(srcAddress); // 作成者
hasher.update(targetAddress); // ターゲット(所有者)
hasher.update(scopeKey); // メタデータのキー
hasher.update(targetId); // 対象(今回はモザイクID)
hasher.update(type: 1); // 1 = モザイクメタデータ
この計算で得られる compositeHash が、このメタデータ固有のIDとなります。
2. Path Hash(ツリー上の住所)の計算
hasher.update(compositeHash);
pathHash = sdkCore.utils.uint8ToHex(hasher.digest());
compositeHash をさらにハッシュ化することで、ステートツリー(16進数の巨大な地図)のどこにデータが格納されているかを示す「住所」を導き出しています。
3. State Hash(値のハッシュ化)
ここでは、メタデータの中身(今回なら "test" という文字列)を含む、メタデータエントリ全体のバイナリを作成してハッシュ化しています。これがツリーの「葉(Leaf)」の中身になります。
4. ルートハッシュの選択
rootHash = blockInfo.data[0].meta.stateHashSubCacheMerkleRoots[8];
Symbolのステートハッシュ(StateHash)は、実は複数の「サブキャッシュ(小部屋)」に分かれています。
- index 0: アカウント
- index 8: メタデータ(← ここを指定しています) このように、データの種類に応じた特定のルートハッシュと比較する必要があります。
このコードが証明すること
検証(checkState)が成功すれば、以下のことが証明されます。
- データの改ざんがない: test という値が、確かにそのモザイクメタデータに設定されている。
- 関係性の正しさ: 指定した「作成者」「所有者」「モザイクID」「キー」の組み合わせに対して、そのデータが紐付いている。
- ノードの信頼性: 接続しているノードが勝手なメタデータを捏造して返していない。
まとめ
このコードは、「特定のモザイクに付けられた付加情報(メタデータ)が、ネットワーク全体で合意された正しいものであること」を、第三者に頼らずに数学的に証明する手順を実装したものです。
13.3.3 アカウントへ登録したメタデータの検証
srcAddress = (new sdkSymbol.Address("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ")).bytes;
targetAddress = (new sdkSymbol.Address("TBIL6D6RURP45YQRWV6Q7YVWIIPLQGLZQFHWFEQ")).bytes;
scopeKey = "9772B71B058127D7"; // scopeKey, メタデータキーを指定
targetId = "0000000000000000"; // targetId
//compositePathHash(Key値)
hasher = sha3_256.create();
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + scopeKey), 8)); // scopeKey, メタデータキーを指定
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + targetId), 8)); // targetId
hasher.update(Uint8Array.from([0])); // type: Account 0
compositeHash = hasher.digest();
hasher = sha3_256.create();
hasher.update( Buffer.from(compositeHash,'hex'));
pathHash = sdkCore.utils.uint8ToHex(hasher.digest());
//stateHash(Value値)
hasher = sha3_256.create();
version = 1;
hasher.update(sdkCore.utils.intToBytes(version, 2)); //version
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + scopeKey), 8)); // scopeKey, メタデータキーを指定
hasher.update(sdkCore.utils.intToBytes(BigInt("0x" + targetId), 8)); // targetId
hasher.update(Uint8Array.from([0])); // account
value = Buffer.from("test"); // メタデータの値を指定
hasher.update(sdkCore.utils.intToBytes(value.length, 2));
hasher.update(value);
stateHash = sdkCore.utils.uint8ToHex(hasher.digest());
//サービス提供者以外のノードから最新のブロックヘッダー情報を取得
query = new URLSearchParams({
"order": "desc"
});
blockInfo = await fetch(
new URL('/blocks?' + query.toString(), NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
rootHash = blockInfo.data[0].meta.stateHashSubCacheMerkleRoots[8];
//サービス提供者を含む任意のノードからマークル情報を取得
stateProof = await fetch(
new URL('/metadata/' + sdkCore.utils.uint8ToHex(compositeHash) + '/merkle', NODE),
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}
)
.then((res) => res.json())
.then((json) => {
return json;
});
//検証
checkState(stateProof,stateHash,pathHash,rootHash);
このコードは、Symbolの「アカウントメタデータ」の正当性を検証する処理です。
先ほどのコードと非常に似ていますが、ターゲットが「モザイク」ではなく「アカウント」になっている点が異なります。Symbolではメタデータの対象によって計算式のフラグが変わるため、そこを厳密に調整して検証を行っています。
前回のコード(モザイクメタデータ)との違い
主要な変更点は以下の3箇所です。
- targetId の扱い targetId = "0000000000000000"アカウントメタデータの場合、特定のモザイクやネームスペースを指さないため、ターゲットIDはすべてゼロ(8バイトの空値)に設定されます。
- メタデータタイプのフラグ hasher.update(Uint8Array.from([0]))ハッシュ計算の末尾で、**0(Account)**を指定しています(モザイクの場合は 1、ネームスペースの場合は 2 でした)。
- Composite Hash の生成「誰が」「誰に」対して「どのキー」で値を書き込んだか、という情報に加えて「タイプ=アカウント」という情報を混ぜることで、アカウントメタデータ専用の「住所(パス)」を導出しています。
まとめ
このコードを実行して検証が通れば、「特定のユーザーが、別のアカウント(または自分自身)に対して設定したプロパティ(メタデータ)が、改ざんなくブロックチェーンに刻まれていること」を数学的に証明したことになります。
これで、速習Symbolは終わりになります。
