以前、マークルパトリシアツリーを活用してアカウント情報の検証を行いました。今回はメタデータを検証してみましょう。
今回はメインネットに実在するメタデータで検証します
メタデータの検証が可能になれば、エクスプローラーなど表示上の改ざんが容易なサービスを利用せずにNFTやDIDなどの所有が証明でき、メタバースなど複数プラットフォームが同居するようなサービスがさらに発展していくと思われます。今、まさに覚えておくべき技術です!
所有者(HOLDER) :targetAddress
NBVHIH5E25AFIRQUYOEMZ35FKEOI275O36YMLZI
発行者(ISSUER) :srcAddress
NCESRRSDSXQW7LTYWMHZOCXAESNNBNNVXHPB6WY
KEY値:F24C633A596D66E7
VALUE値:546869732069732058454D426F6F6B2066756E64206163636F756E742E
今回の検証対象となるメタデータは発行者により所有者のアカウントにメッセージが記録されています。Symbolブロックチェーンのプロトコル上、このメッセージは発行者でなければ変更できません。つまり、所有者はこのメタデータを第三者(検証者)に提示することで、「自分は発行者から承認を得ることができた者です」と主張することができます。
検証に必要な3要素
- KEY-VALUE値
- マークルパトリシアツリー
- ブロックヘッダー
KEY-VALUE値
メタデータそのものです。一つのキーに対して一つのバリュー値が割り当てられます。これにアドレスやその他の区分情報を含めて検証に必要なcompositeHashやstateHashを生成します。
マークルパトリシアツリー
ルート(根)、ブランチ(枝)、リーフ(葉)で構成されます。
ブロックヘッダー
ブロックチェーンによって同期が保証されていることを確認できるヘッダー情報です。
検証したいこと
ブロックヘッダーのハッシュ値が、正しいKEY-VALUE値でないと生成できないことを確認します。
ブロックチェーンの同期はブロックヘッダーのハッシュ値を一致させる必要があるため、同期したノード上では同じKEY-VALUE値を保有していることが期待できます。
前回アカウント情報を検証した時は以下のように、マークルパトリシアツリー上をRawAddressでたどるとブロックヘッダーからアカウント情報(AccountInfo)につながりました。
今回のメタデータではマークルパトリシアツリー上をcompositeHashでたどるとブロックヘッダーからKEY-VALUE値にたどり着けることを確認します。
それでは検証ツアー、スタートです。
環境設定
以下のページのImportとScriptを実行しておいてください。
アドレス生成
所有者、発行者のアドレスを生成しておきます。
srcAddress = sym.Convert.hexToUint8(
sym.Address.createFromRawAddress("NCESRRSDSXQW7LTYWMHZOCXAESNNBNNVXHPB6WY").encoded()
)
targetAddress = sym.Convert.hexToUint8(
sym.Address.createFromRawAddress("NBVHIH5E25AFIRQUYOEMZ35FKEOI275O36YMLZI").encoded()
)
compositeHash生成
マークルパトリシアツリーのルートからリーフまでを導くcompositeHashを生成します。
hasher = sha3_256.create();
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sym.Convert.hexToUint8Reverse("F24C633A596D66E7")); // scopeKey
hasher.update(sym.Convert.hexToUint8Reverse("0000000000000000")); // targetId
hasher.update(Uint8Array.from([0])); // type: Account
compositeHash = hasher.hex();
compositeUint8 = sym.Convert.hexToUint8(compositeHash);
hasher = sha3_256.create();
hasher.update(compositeUint8);
compositePathHash = hasher.hex().toUpperCase();
console.log(compositeHash);
console.log(compositePathHash);
//>'4975ac28ccf0cde03725bafe8ebf9e10ff61e7a4ccaadd85bc8c53197de85e46'
//>'576ED295E1EE4B93C6C9D3A898336A8459335EC0CB238B42353B902F8C31BAAE'
compositeHashとcompositePahHashの2種類を出力します。
SymbolのAPIで検索する場合はcompositeHashで出力可能ですが、
マークルパトリシアツリーの検証においてはcompositePathHashを使用します。
stateHash生成
マークルパトリシアツリーのリーフを構成するstateHashを生成します。
hasher = sha3_256.create();
hasher.update(cat.GeneratorUtils.uintToBuffer(1, 2)); //version
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sym.Convert.hexToUint8Reverse("F24C633A596D66E7")); // scopeKey
hasher.update(sym.Convert.hexToUint8Reverse("0000000000000000")); // targetId
hasher.update(cat.GeneratorUtils.uintToBuffer(0, 1)); //account
value = sym.Convert.utf8ToUint8("546869732069732058454D426F6F6B2066756E64206163636F756E742E");
hasher.update(cat.GeneratorUtils.uintToBuffer(value.length, 2));
hasher.update(value);
stateHash = hasher.hex();
console.log(stateHash)
//>'df776a0140536e2f541a4ee769ed6ccaeb92875f0d921da8c1595c4fb53d0c6e'
compositeHashとの大きな違いはVALUE値を含むことです。
つまりKEY値を元に作成されたcompositeHashは変更されることはありませんが、stateHashは更新されるたびに値が変更されます。
マークル情報と最新ブロックヘッダーの取得
SymbolブロックチェーンのREST APIを経由してマークル情報とブロックヘッダーを取得します。
rxjs.zip(
stateProofService.metadataById(compositeHash),
blockRepo.search({order:"desc"})
).subscribe(x=>{
state = x[0]; // stateProof
console.log(state.rootHash);
console.log(x[1].data[0].stateHashSubCacheMerkleRoots[8]);
})
//>8F068D1D9A536D9C94123C9884D17BF50C53A5D59DED89D9C0C76D9890C87158
//>8F068D1D9A536D9C94123C9884D17BF50C53A5D59DED89D9C0C76D9890C87158
今回の記事で唯一ブロックチェーンに情報を取りに行く部分です。
どのノードに接続するかが重要になります。情報提供者が改ざんを行わないように、検証者は任意のノードを自分で選択して情報を取得します。
マークル情報をstateに代入し、rootHash値が最新ブロックヘッダーの
stateHashSubCacheMerkleRoots[8]に等しいことを確認しておきます。
ツリーをたどる。
function getLeafHash(encodedPath, leafValue){
hasher = sha3_256.create();
return hasher.update(sym.Convert.hexToUint8(encodedPath + leafValue)).hex().toUpperCase();
}
function getBranchHash(encodedPath, links){
const branchLinks = Array(16).fill(sym.Convert.uint8ToHex(new Uint8Array(32)));
links.forEach((link) => {
branchLinks[parseInt(`0x${link.bit}`, 16)] = link.link;
});
hasher = sha3_256.create();
bHash = hasher.update(sym.Convert.hexToUint8(encodedPath + branchLinks.join(''))).hex().toUpperCase();
console.log(encodedPath + branchLinks.join(''));
console.log(bHash);
return bHash;
}
merkleBranches = state.merkleTree.branches.reverse();
leafHash = getLeafHash(state.merkleTree.leaf.encodedPath,stateHash)
branchHash = leafHash;
bit = "";
for(let i = 0; i < merkleBranches.length; i++){
branch = merkleBranches[i];
merkleTreeBranchLink = branch.links.find(x=>x.link === branchHash)
branchHash = getBranchHash(branch.encodedPath,branch.links);
bit = merkleTreeBranchLink.bit + bit;
}
pathHash = bit + state.merkleTree.leaf.path;
rootHash = branchHash;
console.log("rootHash:" + rootHash);
console.log(pathHash);
console.log(compositePathHash);
//>8F068D1D9A536D9C94123C9884D17BF50C53A5D59DED89D9C0C76D9890C87158
//>576ED295E1EE4B93C6C9D3A898336A8459335EC0CB238B42353B902F8C31BAAE0
葉からルートへの順番で辿っていきます。
まず葉のハッシュ値がstateHashと一致することを確認します。次に、その葉のハッシュ値が、一つ手前の枝がもつLinkリストのどれか一つと当てはまります。繰り返して、その枝のハッシュ値がもう一つ手前の枝が持つLinkリストのどれかと当てはまります。当てはまったリストが持つbit値を都度控えておき、ルートにたどり着いたときにすべて結合します。
検証する
if(pathHash.indexOf(compositePathHash) >= 0){
console.log("valid");
}
マークルパスをたどって集めた枝値と葉値の結合値(pathHash)がCompositeHashを含むことを確認します。
これでKEY-VALUE値からハッシュ化した情報の派生でブロックチェーンのルートハッシュが作られていることが分かりました。
さいごに
お疲れ様でした。これで検証者はメタデータの所有者が提示する情報に間違いがないことを確認できました。今回の検証で最も重要な点は、ノードと検証者の徹底した分離です。信頼性検証がサービス提供者のノードに依存しているかぎりプラットフォームをまたいだサービスを実現することはできません。
今後NFTやDIDがさらに普及し始めると、メタデータの検証は必ず重要になります。検証者に必要なのは自前で準備した保守運用の行き届いたノードではなく、みんなが正しいと認め合うファイナライズブロックただそれだけです。
電波時計でファイナライズブロックを受信する日がくるかもしれませんね。