13.検証
ブロックチェーン上に記録されたさまざまな情報を検証します。 ブロックチェーンへのデータ記録は全ノードの合意を持って行われますが、 ブロックチェーンへのデータ参照はノード単体からの情報取得であるため、 信用できないノードの情報を元にして新たな取引を行いたい場合は、ノードから取得したデータに対して検証を行う必要があります。
13.1 トランザクションの検証
トランザクションがブロックヘッダーに含まれていることを検証します。この検証が成功すれば、トランザクションがブロックチェーンの合意によって承認されたものとみなすことができます。
本章のサンプルスクリプトを実行する前に以下を実行して必要ライブラリを読み込んでおいてください。
sha3_256 = (await import("https://cdn.skypack.dev/@noble/hashes/sha3"))
.sha3_256;
検証するペイロード
今回検証するトランザクションペイロードとそのトランザクションが記録されているとされるブロック高です。
payload =
"2802000000000000A5151FD55D82351DD488DB5563DD328DA72B2AD25B513C1D0F7F78AFF4D35BA094ABF505C74E6D6BE1FA19F3E5AC60A85E1A4EDC4AC07DECC0E56C59D5D24F0B69A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB0000000002984141A0D70000000000000EEAD6810500000062E78B6170628861B4FC4FCA75210352ACDBD2378AC0A447A3DCF63F969366BB1801000000000000540000000000000069A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB000000000198544198A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329D04000000000000000074783100000000590000000000000069A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB000000000198444198A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329D6668A0DE72812AAE05000500746573743100000000000000590000000000000069A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB000000000198444198A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329DBF85DADBFD54C48D050005007465737432000000000000000000000000000000662CEDF69962B1E0F1BF0C43A510DFB12190128B90F7FE9BA48B1249E8E10DBEEDD3B8A0555B4237505E3E0822B74BCBED8AA3663022413AFDA265BE1C55431ACAE3EA975AF6FD61DEFFA6A16CBA5174A16EF5553AE669D5803A0FA9D1424600";
height = 686312;
payload確認
トランザクションの内容を確認します。
tx = symbolSdk.SymbolTransactionFactory.deserialize(
sdkCore.utils.hexToUint8(payload),
);
hash = facade.hashTransaction(tx);
console.log(hash);
console.log(tx);
出力例
> Hash256 {bytes: Uint8Array(32)}
> AggregateCompleteTransactionV2
> cosignatures: Array(1)
> 0: Cosignature
signature: Signature {bytes: Uint8Array(64)}
signerPublicKey: PublicKey {bytes: Uint8Array(32)}
deadline: Timestamp {size: 8, isSigned: false, value: 23653181966n}
fee: Amount {size: 8, isSigned: false, value: 55200n}
network: NetworkType {value: 152}
signature: Signature {bytes: Uint8Array(64)}
signerPublicKey: PublicKey {bytes: Uint8Array(32)}
size: 552
> transactions: Array(3)
0: EmbeddedTransferTransactionV1 {_signerPublicKey: PublicKey, _version: 1, _network: NetworkType, _type: TransactionType, _recipientAddress: UnresolvedAddress, …}
1: EmbeddedAccountMetadataTransactionV1 {_signerPublicKey: PublicKey, _version: 1, _network: NetworkType, _type: TransactionType, _targetAddress: UnresolvedAddress, …}
2: EmbeddedAccountMetadataTransactionV1 {_signerPublicKey: PublicKey, _version: 1, _network: NetworkType, _type: TransactionType, _targetAddress: UnresolvedAddress, …}
transactionsHash: Hash256 {bytes: Uint8Array(32)}
type: TransactionType {value: 16705}
version: 2
署名者の検証
トランザクションがブロックに含まれていることが確認できれば自明ですが、
念のため、アカウントの公開鍵でトランザクションの署名を検証しておきます。
res = facade.verifyTransaction(tx, tx.signature);
console.log(res);
> true
マークルコンポーネントハッシュの計算
トランザクションのハッシュ値には連署者の情報が含まれていません。
一方でブロックヘッダーに格納されるマークルルートはトランザクションのハッシュに連署者の情報が含めたものが格納されます。
そのためトランザクションがブロック内部に存在しているかどうかを検証する場合は、トランザクションハッシュをマークルコンポーネントハッシュに変換しておく必要があります。
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);
出力例
> C61D17F89F5DEBC74A98A1321DB71EB7DC9111CDF1CF3C07C0E9A91FFE305AC3
InBlockの検証
ノードからマークルツリーを取得し、先ほど計算したmerkleComponentHashからブロックヘッダーのマークルルートが導出できることを確認します。
//トランザクションから計算
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 = symbolSdk.proveMerkle(leaf, merkleProof, HRoot);
console.log(result);
出力例
> true
トランザクションの情報がブロックヘッダーに含まれていることが確認できました。
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 === symbolSdk.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(
Buffer.from(
block.version.toString(16).padStart(1 * 2, "0"),
"hex",
).reverse(),
);
hasher.update(
Buffer.from(
block.network.toString(16).padStart(1 * 2, "0"),
"hex",
).reverse(),
);
hasher.update(
Buffer.from(block.type.toString(16).padStart(2 * 2, "0"), "hex").reverse(),
);
hasher.update(
Buffer.from(
BigInt(block.height)
.toString(16)
.padStart(8 * 2, "0"),
"hex",
).reverse(),
);
hasher.update(
Buffer.from(
BigInt(block.timestamp)
.toString(16)
.padStart(8 * 2, "0"),
"hex",
).reverse(),
);
hasher.update(
Buffer.from(
BigInt(block.difficulty)
.toString(16)
.padStart(8 * 2, "0"),
"hex",
).reverse(),
);
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(
Buffer.from(
block.feeMultiplier.toString(16).padStart(4 * 2, "0"),
"hex",
).reverse(),
);
hash = sdkCore.utils.uint8ToHex(hasher.digest());
console.log(hash === blockInfo.meta.hash);
}
true が出力されればこのブロックハッシュは前ブロックハッシュ値の存在を認知していることになります。
同様にしてn番目のブロックがn-1番目のブロックを存在を確認し、最後に検証中のブロックにたどり着きます。
これで、どのノードに問い合わせても確認可能な既知のファイナライズブロックが、
検証したいブロックの存在に支えられていることが分かりました。