速習Symbolブロックチェーン13章、検証編です。

URLはこちら。

冒頭に「トランザクションの認証は全ノードの合意が行われている」が、「データ参照はノード単体からの情報取得」のため、変なノードが変なデータを返して来たら欺かれるかもよ? という話です。

実際にどれぐらいの可能性でそういうことになるかはわからないですが、可能か不可能かでいうと可能でしょうし、
検証こそブロックチェーンの華? であるため、実施していきます。

実践編

初めに。(という名の言い訳等)

元々「速習SymbolをFlutterで実行してやろう」という思いでここまでやってきましたが、ここにきて一部予定を変更することとなりました。。。
これも、僕のモチベーションと進捗の問題かな? と思っています。

一部変更する予定についてですが、
「13.3 アカウント・メタデータの検証」をFlutterでなく、速習Symbolの通りに実行することとしました。

  • もともとの予定として2023年内に終わることを目指していたこと。
  • AccountInfoDTOのserialize()の内容を作るのが困難(少なくとも、上記の予定に間に合わない)こと。

以上の理由から、「13.3 アカウント・メタデータの検証」は速習Symbolの通りのまま、実施したいと思います。
機会があれば、いずれやってみるという感じで。。。たぶん。。。はい。。。

※ぱっと見、AccountStateBuilderとかいまいち不明で。
AccountStateなどはv3のcatbufferなどに記載があったと思うので、コードを生成すればできあがるのかもしれませんが🤔

まー、コードの全容も明かしていないため自分以外には特に意味のない記事ではあるため、、、まぁそんな感じででしてね。はい。

それではやっていきます。

13.1 トランザクションの検証

初めに。

速習Symbolに掲載されていた、”検証で使用するペイロードとトランザクションハッシュ”についてですが、僕には見つけることができませんでした。

ブロックでもトランザクションハッシュでも見つからず、また、速習Symbol通りに検証を行っても署名者の検証で”true”となりませんでした。
※速習Symbolではaliceで署名の検証を行っていましたが、その”alice”がわからなかった。。
 ので、ペイロードから復元したsignerPublicKeyとsignature、あとgenerationHashSeedで検証してみるもだめでした。

よって、前回のオフライントランザクション編のものを再度実行し、そこからペイロードを取得し、それを対象に確認していこうと思います。

ペイロードはこれ。

C00100000000000034333983EB9380A79C4575B56FA31AF39629AD9D601090257B188B2CFC2C9ABA8E1620F1B1383F470ADB32BD88CC2F814A3F6A96FB71D8A5146DD8BB2EC6890867626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28000000000298414100AF000000000000284A9752080000009979F2E9DA1540D795A05BAA3458273A7420342387A4437EC096B6A0E17FA13AB000000000000000540000000000000067626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A2800000000019854419873F010CF581FAC0AAF94BC24C75F255D0FB380BD1601470400000000000000007478310000000054000000000000003ED443924F5C524611290BFB434CBAE5BCDC85975D98F27A138EC5002DC20A8500000000019854419808ACFBB0A6E7C3AB692111A93807C35F80D33F0BC5CC000400000000000000007478320000000000000000000000003ED443924F5C524611290BFB434CBAE5BCDC85975D98F27A138EC5002DC20A85CBE7B0741363ECECA9FBA330887006BAD77D13F6E79BA0DF9B32E99241B5097CBEF8D6B00643DA119A733A5D37515FA54A96A05CD0E62606A4239D5F374FC000

トランザクションハッシュはこれ。

ce63d09e479e4b1e7061815dfa9353f4b157cb8a01ff1389e70eb53b61280ca4

ブロック高はこれ。

1061794

です。

また、全体のコードは長いため、部分ごとに掲載していきます。

payload確認

コンソールに直接全体を出せるような、連想配列化とかしていないため、出力がちょっと面倒な感じになっています。


// 検証対象となるペイロードとブロック高。
var payload = 'C00100000000000034333983EB9380A79C4575B56FA31AF39629AD9D601090257B188B2CFC2C9ABA8E1620F1B1383F470ADB32BD88CC2F814A3F6A96FB71D8A5146DD8BB2EC6890867626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28000000000298414100AF000000000000284A9752080000009979F2E9DA1540D795A05BAA3458273A7420342387A4437EC096B6A0E17FA13AB000000000000000540000000000000067626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A2800000000019854419873F010CF581FAC0AAF94BC24C75F255D0FB380BD1601470400000000000000007478310000000054000000000000003ED443924F5C524611290BFB434CBAE5BCDC85975D98F27A138EC5002DC20A8500000000019854419808ACFBB0A6E7C3AB692111A93807C35F80D33F0BC5CC000400000000000000007478320000000000000000000000003ED443924F5C524611290BFB434CBAE5BCDC85975D98F27A138EC5002DC20A85CBE7B0741363ECECA9FBA330887006BAD77D13F6E79BA0DF9B32E99241B5097CBEF8D6B00643DA119A733A5D37515FA54A96A05CD0E62606A4239D5F374FC000';
var height = Height(1061794);

print('検証対象するペイロード');
print(payload);
print('検証対象のブロック高');
print(height);

print('payload確認');

// 内容は不明。
var deserializedTx = DeserializedTransaction.deserialize(payload, generationHashSeed);

// トランザクションと設定情報を取得する。(設定情報は本来のSDKには概念がない)
var desTx = deserializedTx.transaction;
var desTxSetting = deserializedTx.deserializedSetting;

// ここではAggregateが判明している様子。一応トラップする。
if (desTx is! AggregateTransaction) return;

print('適当にデシリアライズした値を出力する。');
print('AggregateCompleteTransactionか?: ${desTx.txInfo is AggregateCompleteTxInfo ? 'yes' : 'no' }');
print('トランザクションサイズ: ${desTx.getTransactionSize()}');
print('Tx fee: ${desTxSetting.fee.value.toInt()}');
print('署名者: ${desTxSetting.signer.address}');
print('署名: ${desTxSetting.signature.value}');
print('NetworkType: ${desTxSetting.networkType.value}');
print('innerTxの出力');
for (var innerTx in desTx.txInfo.transactions){
  // 本来ではすべてのTxの型を判定したいが省略し、ここでは使用しているもの(Transfer, AccountMetadata)のみとする。

  switch (innerTx.transactionType) {
    case TransactionType.transfer:                  // 転送
      innerTx as TransferTxInfo;
      print('送信元アドレス: ${innerTx.publicAccount.address.value}');
      print('送信先アドレス: ${innerTx.recipientAddress.value}');
      
      for (var element in innerTx.transferMosaics){
        print('mosaicId: ${element.id.value}, mount: ${element.amount.value}');
      }

      print('メッセージ: ${innerTx.message.message}');
      break;

    default:
      throw Exception('Unsupport transaction type.');
  }
}
print('連署者数: ${desTx.cosignatures.length}');
for (var element in desTx.cosignatures){
  print(element.signerPublicKey.toRawAddress(NetworkType.testnet).toAddress().value);
}

結果。

ペイロードからのデシリアライズを行いました。

  • AggregateCompleteTransactionである。
  • Txサイズが448であり、TxFeeが44800であるから、FeeMultiplierは100であること。
  • NetworkTypeがテストネット(152)であること。
  • InnerTxの内容は転送2件で、TAEK…とTBZ7…のアドレスが転送しあっており、メッセージがそれぞれtx1、tx2であること。
  • 連署者数が1であり、関係しているアドレスTBZ7…であること。

がメッセージ上、わかります。

署名者の検証

ペイロードから復元したトランザクション情報には署名と署名者公開鍵が設定されています。

署名はトランザクションの内容に対して行われており、公開鍵さえあればその署名は対象の公開鍵(アドレス)で行われているか検証可能です。

以下、コード。

print('-----');
print('署名者の検証');

// 速習Symbolではaliceを参照して行っているが、
// 手元にaliceの秘密鍵は存在しないため、公開情報のみで検証を行う。
// すべてデシリアライズしているため、そこから検証を行う。

print('署名は正しいか?');
var res = await desTxSetting.signer.verifyByte(desTx.getSigningBytes(generationHashSeed), desTxSetting.signature);
print(res ? 'はい' : 'いいえ');

コードコメントの通り、速習Symbolではaliceがわかっていて行われていました。
しかし、aliceは不明であるという前提で、ペイロードから復元した署名者の公開鍵と署名で検証を行いました。

署名自体はトランザクションの(検証対象とする)内容とgenerationHashSeedを対象に行われており、その対象と署名から、署名者の公開鍵が署名を行ったか? が検証できるようになっています。

これにより、トランザクションの内容に署名したのは、ペイロードに設定されている署名者の公開鍵であることが確認できます。

マークルコンポーネントハッシュの計算

トランザクションがブロック内部に存在するかどうかはマークルコンポーネントハッシュを計算する必要があるようです。

print('-----');
print('マークルコンポーネントハッシュの計算');

var hasher = SHA3(256, SHA3_PADDING, 256);
hasher.update(desTx.txHash.toByte());
for (var element in desTx.cosignatures){
  hasher.update(element.signerPublicKey.toByte());
}
var merkleComponentHash = hex.encode(hasher.digest());

print(merkleComponentHash);

結果。

トランザクションハッシュ、および連署者の公開鍵でハッシュ値を生成するようです。

InBlockの検証

ノードからマークルツリーを取得し、そのブロックヘッダーのマークルルートが、先ほどのmerkleComponentHashから導出されるかを調べます。

print('InBlockの検証');
// ノードからマークルツリーを取得し、先ほどのマークルコンポーネントハッシュから、
// ブロックヘッダーのマークルルートが導出できることを確認するらしい。

validateTransactionInBlock(Hash256 leaf, Hash256 root, List<MerklePathItemDTO> merkleProof){

  if (merkleProof.isEmpty) {
    // There is a single item in the tree, so HRoot' = leaf.
    return leaf.value.toUpperCase() == root.value.toUpperCase();
  }

  var hRoot0 = merkleProof.fold<String>(leaf.value, (proofHash, pathItem) {
    final hasher = SHA3(256, SHA3_PADDING, 256);
    if (pathItem.position == Position.left) {
      return hex.encode(hasher.update(hex.decode(pathItem.hash!.value + proofHash)).digest());
    } else {
      return hex.encode(hasher.update(hex.decode(proofHash + pathItem.hash!.value)).digest());
    }
  });

    return root.value.toUpperCase() == hRoot0.toUpperCase();
}

// 接続先ホストを取得する。
var networkHostInfo = await TransactionSender.getNetworkHost();

var blockHttp = BlockRoutesHttp(networkHostInfo.networkHost);

var leaf = Hash256(merkleComponentHash);
var hogehoge = await blockHttp.getInformation(height);
var hRoot = hogehoge.block.transactionsHash;
var merkleProof = (await blockHttp.getMerklePathGivenTransactionAndBlock(height, leaf)).merklePath;

var validateResult = validateTransactionInBlock(leaf, hRoot, merkleProof);

print('導出${validateResult ? 'できた' : 'できない'}');

結果。

導出できました。

ただ、これ、merkleProofが0件だったんですよね。。。確か。
よって、コードの一部は通っていないです。

本当は今回のmerkleComponentHashと、同じブロックに含まれるmerkleProofのハッシュによって、ブロックヘッダーのマークルルートが導出され、「はい、含まれてる証明になったね」っていう感じだと思うのですが。。。
これはブロックがスカスカだからですかね?

本項のコード全体です。

// 検証対象となるペイロードとブロック高。
var payload = 'C00100000000000034333983EB9380A79C4575B56FA31AF39629AD9D601090257B188B2CFC2C9ABA8E1620F1B1383F470ADB32BD88CC2F814A3F6A96FB71D8A5146DD8BB2EC6890867626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A28000000000298414100AF000000000000284A9752080000009979F2E9DA1540D795A05BAA3458273A7420342387A4437EC096B6A0E17FA13AB000000000000000540000000000000067626D0C01D6E24F7F237072C422D81A130E3A429A6C9CFFF45B5CFCBBDC9A2800000000019854419873F010CF581FAC0AAF94BC24C75F255D0FB380BD1601470400000000000000007478310000000054000000000000003ED443924F5C524611290BFB434CBAE5BCDC85975D98F27A138EC5002DC20A8500000000019854419808ACFBB0A6E7C3AB692111A93807C35F80D33F0BC5CC000400000000000000007478320000000000000000000000003ED443924F5C524611290BFB434CBAE5BCDC85975D98F27A138EC5002DC20A85CBE7B0741363ECECA9FBA330887006BAD77D13F6E79BA0DF9B32E99241B5097CBEF8D6B00643DA119A733A5D37515FA54A96A05CD0E62606A4239D5F374FC000';
var height = Height(1061794);

print('検証対象するペイロード');
print(payload);
print('検証対象のブロック高');
print(height);

print('payload確認');

// 内容は不明。
var deserializedTx = DeserializedTransaction.deserialize(payload, generationHashSeed);

// トランザクションと設定情報を取得する。(設定情報は本来のSDKには概念がない)
var desTx = deserializedTx.transaction;
var desTxSetting = deserializedTx.deserializedSetting;

// ここではAggregateが判明している様子。一応トラップする。
// (本来はちゃんと判定する必要があるよね)
if (desTx is! AggregateTransaction) return;

print('適当にデシリアライズした値を出力する。');
print('AggregateCompleteTransactionか?: ${desTx.txInfo is AggregateCompleteTxInfo ? 'yes' : 'no' }');
print('トランザクションサイズ: ${desTx.getTransactionSize()}');
print('Tx fee: ${desTxSetting.fee.value.toInt()}');
print('署名者: ${desTxSetting.signer.address}');
print('署名: ${desTxSetting.signature.value}');
print('NetworkType: ${desTxSetting.networkType.value}');
print('innerTxの出力');
for (var innerTx in desTx.txInfo.transactions){
  // 本来ではすべてのTxの型を判定したいが省略し、ここでは使用しているもの(Transfer, AccountMetadata)のみとする。

  switch (innerTx.transactionType) {
    case TransactionType.transfer:                  // 転送
      innerTx as TransferTxInfo;
      print('送信元アドレス: ${innerTx.publicAccount.address.value}');
      print('送信先アドレス: ${innerTx.recipientAddress.value}');
      
      for (var element in innerTx.transferMosaics){
        print('mosaicId: ${element.id.value}, mount: ${element.amount.value}');
      }

      print('メッセージ: ${innerTx.message.message}');
      break;
    case TransactionType.accountMetadata:           // アカウントメタデータ。   
      innerTx as AccountMetadataTxInfo;
      // ペイロードから生成されたキーや値は変換済みのため、ペイロード単体では元の値は不明。
      print('登録対象アドレス: ${innerTx.targetAddress.value}');
      print('ScopeMetadataKey: ${innerTx.scopeMetadataKey.value}');
      break;
    default:
      throw Exception('Unsupport transaction type.');
  }
}
print('連署者数: ${desTx.cosignatures.length}');
for (var element in desTx.cosignatures){
  print(element.signerPublicKey.toRawAddress(NetworkType.testnet).toAddress().value);
}

print('-----');
print('署名者の検証');

// 速習Symbolではaliceを参照して行っているが、
// 手元にaliceの秘密鍵は存在しないため、公開情報のみで検証を行う。
// すべてデシリアライズしているため、そこから検証を行う。

print('署名は正しいか?');
var res = await desTxSetting.signer.verifyByte(desTx.getSigningBytes(generationHashSeed), desTxSetting.signature);
print(res ? 'はい' : 'いいえ');

print('-----');
print('マークルコンポーネントハッシュの計算');

var hasher = SHA3(256, SHA3_PADDING, 256);
hasher.update(desTx.txHash.toByte());
for (var element in desTx.cosignatures){
  hasher.update(element.signerPublicKey.toByte());
}
var merkleComponentHash = hex.encode(hasher.digest());

print(merkleComponentHash);

print('InBlockの検証');
// ノードからマークルツリーを取得し、先ほどのマークルコンポーネントハッシュから、
// ブロックヘッダーのマークルルートが導出できることを確認するらしい。

validateTransactionInBlock(Hash256 leaf, Hash256 root, List<MerklePathItemDTO> merkleProof){

  if (merkleProof.isEmpty) {
    // There is a single item in the tree, so HRoot' = leaf.
    return leaf.value.toUpperCase() == root.value.toUpperCase();
  }

  var hRoot0 = merkleProof.fold<String>(leaf.value, (proofHash, pathItem) {
    final hasher = SHA3(256, SHA3_PADDING, 256);
    if (pathItem.position == Position.left) {
      return hex.encode(hasher.update(hex.decode(pathItem.hash!.value + proofHash)).digest());
    } else {
      return hex.encode(hasher.update(hex.decode(proofHash + pathItem.hash!.value)).digest());
    }
  });

    return root.value.toUpperCase() == hRoot0.toUpperCase();
}

// 接続先ホストを取得する。
var networkHostInfo = await TransactionSender.getNetworkHost();

var blockHttp = BlockRoutesHttp(networkHostInfo.networkHost);

var leaf = Hash256(merkleComponentHash);
var hogehoge = await blockHttp.getInformation(height);
var hRoot = hogehoge.block.transactionsHash;
var merkleProof = (await blockHttp.getMerklePathGivenTransactionAndBlock(height, leaf)).merklePath;

var validateResult = validateTransactionInBlock(leaf, hRoot, merkleProof);

print('導出${validateResult ? 'できた' : 'できない'}');

これで

  • トランザクションの内容と、それに対して誰が(どの公開鍵・アドレス)が署名したのか?
  • そのトランザクションから生成したmerkleComponentHashから、ノードのブロックヘッダーのマークルルートを導出することで、そのトランザクションはすでにブロックに含まれているか?

の検証と証明ができました。

13.2 ブロックヘッダーの検証

既知のブロックハッシュ値から検証中のブロックヘッダーまで辿れることを検証するようです。

ざっくり言えば、ブロックハッシュはそれ単体で導出されるわけではなく、前のブロックの状態を含めてハッシュ値を導出しているようです。
つまり今現在のブロックのハッシュ値は過去のものの積み上げの上に計算されている、ということです。

ここではそこまで検証しませんが、言い換えれば最初のブロックからひとつひとつ計算していけば、現在のブロックのハッシュ値が導出されます。そうでない場合は途中に異常なデータが混入している、というところでしょうか。

normalブロックの検証

1061794を対象に検証します。
なお、REST APIを直接叩くと現在のブロック-1のハッシュ値も取得できるようになっています。
そのため、問い合わせは一度のみです。

print('normalブロックの検証');

var height = Height(1061794);
// REST APIから叩くと、直前ブロックのハッシュも取得できる。
// var previousHeight = height - Height(1);

print('検証対象のブロック高');
print(height);

var blockInfo = await blockHttp.getInformation(height);

if(blockInfo.block.type == BlockType.normal.value){
  
  var hasher = SHA3(256, SHA3_PADDING, 256);

  hasher.update(blockInfo.block.signature.toByte());
  hasher.update(blockInfo.block.signerPublicKey.toByte());
  hasher.update(Int8(blockInfo.block.version).toByte());
  hasher.update(blockInfo.block.network.toByte());
  hasher.update(Int16(blockInfo.block.type).toByte());
  hasher.update(blockInfo.block.height.toByte());
  hasher.update(blockInfo.block.timestamp.toByte());
  hasher.update(blockInfo.block.difficulty.toByte());
  hasher.update(hex.decode(blockInfo.block.proofGamma.value));
  hasher.update(hex.decode(blockInfo.block.proofVerificationHash.value));
  hasher.update(hex.decode(blockInfo.block.proofScalar.value));
  hasher.update(blockInfo.block.previousBlockHash.toByte());
  hasher.update(blockInfo.block.transactionsHash.toByte());
  hasher.update(blockInfo.block.receiptsHash.toByte());
  hasher.update(blockInfo.block.stateHash.toByte());
  hasher.update(hex.decode(blockInfo.block.beneficiaryAddress.value));
  hasher.update(blockInfo.block.feeMultiplier.value.toInt32().toBytes()); // 64bitじゃない?
  var result = hex.encode(hasher.digest()).toUpperCase();
  print("result hash: $result");
  print("導出 ${result == blockInfo.meta.hash.value ? 'した' : 'しない'}");

}

結果です。

導出されました。

途中でpreviousBlockHashが入っているため、これで前ブロックのハッシュ値を含めていることがわかります。
※あれこれ、ここも偽られたら問題だから、速習Symbolでは別途、前のブロックを取得してたのかな? どうなんだろう。

importanceブロックの検証

importanceブロックも同様に検証します。ただし、ハッシュに含める値が追加されているようです。

print('importanceブロックの検証');

var height = Height(1066500);

var blockInfo = await blockHttp.getInformation(height);

if(blockInfo.block.type == BlockType.importance.value){

  var block = blockInfo.block as ImportanceBlockDTO;

  var hasher = SHA3(256, SHA3_PADDING, 256);
  hasher.update(block.signature.toByte());
  hasher.update(block.signerPublicKey.toByte());
  hasher.update(Int8(block.version).toByte());
  hasher.update(block.network.toByte());
  hasher.update(Int16(block.type).toByte());
  hasher.update(block.height.toByte());
  hasher.update(block.timestamp.toByte());
  hasher.update(block.difficulty.toByte());
  hasher.update(hex.decode(block.proofGamma.value));
  hasher.update(hex.decode(block.proofVerificationHash.value));
  hasher.update(hex.decode(block.proofScalar.value));
  hasher.update(block.previousBlockHash.toByte());
  hasher.update(block.transactionsHash.toByte());
  hasher.update(block.receiptsHash.toByte());
  hasher.update(block.stateHash.toByte());
  hasher.update(hex.decode(block.beneficiaryAddress.value));
  hasher.update(block.feeMultiplier.value.toInt32().toBytes());         // 64bitじゃない?
  hasher.update(Int64(block.votingEligibleAccountsCount).toInt32().toBytes());
  hasher.update(block.harvestingEligibleAccountsCount.toBytes());
  hasher.update(block.totalVotingBalance.toByte());
  hasher.update(block.previousImportanceBlockHash.toByte());
  var result = hex.encode(hasher.digest()).toUpperCase();
  print("result hash: $result");
  print("導出 ${result == blockInfo.meta.hash.value ? 'した' : 'しない'}");

}

結果です。

導出されたようです。

stateHashの検証

normal/importanceブロックともにハッシュの導出について、stateHashというものが使用されていました。

hasher.update(block.stateHash.toByte());

stateHashというのは、

  • AccountState
  • Namespace
  • Mosaic
  • Multisig
  • HashLockInfo
  • SecretLockInfo
  • AccountRestriction
  • MosaicRestriction
  • Metadata

のそれぞれのstateHashSubCacheMerkleRootsから構成されているようです。名前と雰囲気からして、それぞれのトランザクションに影響ありそうですね。
過去のトランザクションすべてがそれぞれに積み上げられて、ハッシュとされているのでしょうか。

print('StateHashの検証');

var height = Height(1061794);

print('検証対象のブロック高');
print(height);

var blockInfo = await blockHttp.getInformation(height);

print('block内容');
print(blockInfo.baseMap);

// 0: AccountState
// 1: Namespace
// 2: Mosaic
// 3: Multisig
// 4: HashLockInfo
// 5: SecretLockInfo
// 6: AccountRestriction
// 7: MosaicRestriction
// 8: Metadata

var hasher = SHA3(256, SHA3_PADDING, 256);
for(var subCache in blockInfo.meta.stateHashSubCacheMerkleRoots){
  hasher.update(subCache.toByte());
}

var result = hex.encode(hasher.digest()).toUpperCase();
print("result hash: $result");
print("導出 ${result == blockInfo.block.stateHash.value ? 'した' : 'しない'}");

結果。

導出されました。

13.3 アカウント・メタデータの検証

さて、冒頭の通り、ここからは速習Symbolの通りに実施します。

といっても、速習Symbolの通りに実行するだけであれば特に書くこともなくなってしまうのですが。。。

一応コンソールの内容でも張っていきます。

13.3.1 アカウント情報の検証

アカウント情報を葉として、マークルツリーのルートに到着できるかを確認するようです。

導出されました。

これを検証してなにがどうなのかいまいちよくわかっていませんが、「アカウントがチェーン上に認知されている存在であり、正しく取り込まれている情報であるということがわかる」というところでしょうか?
うーん、違う気がする。自信がないですね。

例えば、、、

  • サービス提供者のノードからアカウント情報(alice)を取得する。
  • サービス提供者のノードから取得したアカウント情報(alice)が、パブリックチェーン(対象のチェーン)で正しく存在するか怪しい
    (サービス提供者のノードがネットワーク/ブロック情報を偽っている可能性もある)
  • サービス提供者とは関係のない第三者のノードから、最新のブロックヘッダー情報を取得する
  • アカウント情報をもとに、マークル情報を取得し、ルートのハッシュ値を導出する
  • アカウント情報からマークル情報をたどり、最新のブロックヘッダーのハッシュ値が導出されれば、「サービス提供者のノードから取得したアカウント情報(alice)は正しい」といえる

でしょうか? うーん、わからん。

13.3.2 モザイクへ登録したメタデータの検証

モザイクへ登録したメタデータの検証として、アカウントと同様のことを行い、検証するようです。

元々のコードから、address、metadataScopeKey、targetId、valueの値を変更しました(Notfoundと出てしまったため)。

ここまでの速習Symbolで作成したアカウントおよびモザイク、それに紐づいたメタデータを使用しています。

srcAddress = Buffer.from(
    sym.Address.createFromRawAddress("TAEKZ65QU3T4HK3JEEI2SOAHYNPYBUZ7BPC4YAA").encoded(),
    'hex'
)

targetAddress = Buffer.from(
    sym.Address.createFromRawAddress("TAEKZ65QU3T4HK3JEEI2SOAHYNPYBUZ7BPC4YAA").encoded(),
    'hex'
)

hasher = sha3_256.create();    
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sym.Convert.hexToUint8Reverse("E3CCA82CDB0EDFF4")); // scopeKey
hasher.update(sym.Convert.hexToUint8Reverse("50E29F813AA1B901")); // targetId
hasher.update(Uint8Array.from([sym.MetadataType.Mosaic])); // type: Mosaic 1
compositeHash = hasher.hex();

hasher = sha3_256.create();   
hasher.update( Buffer.from(compositeHash,'hex'));

pathHash = hasher.hex().toUpperCase();

//stateHash(Value値)
hasher = sha3_256.create(); 
hasher.update(cat.GeneratorUtils.uintToBuffer(1, 2)); //version
hasher.update(srcAddress);
hasher.update(targetAddress);
hasher.update(sym.Convert.hexToUint8Reverse("E3CCA82CDB0EDFF4")); // scopeKey
hasher.update(sym.Convert.hexToUint8Reverse("50E29F813AA1B901")); // targetId
hasher.update(Uint8Array.from([sym.MetadataType.Mosaic])); //mosaic

value = Buffer.from("gai");

hasher.update(cat.GeneratorUtils.uintToBuffer(value.length, 2)); 
hasher.update(value); 
stateHash = hasher.hex();

//サービス提供者以外のノードから最新のブロックヘッダー情報を取得
blockInfo = await blockRepo.search({order:"desc"}).toPromise();
rootHash = blockInfo.data[0].stateHashSubCacheMerkleRoots[8];

//サービス提供者を含む任意のノードからマークル情報を取得
stateProof = await stateProofService.metadataById(compositeHash).toPromise();

//検証
checkState(stateProof,stateHash,pathHash,rootHash);

結果です。

導出されました。

13.3.3 アカウントへ登録したメタデータの検証

同様にアカウントへ登録したメタデータを対象に、導出できるか検証します。

モザイクと同様に、address、scopeMetadataKey、valueを変更しています。

導出されました。

アカウントへ登録したメタデータが正常に取り込まれているデータである、ということが確認できる。という認識でいいんでしょうか?

13.4 現場で使えるヒント

トラステッドウェブ、ということについて書かれています。

Symbolではすべてのブロック情報が正しいかどうかを外部から検証可能である。

毎度、チェーンの利用者がチェーンのブロック情報が正しいかを検証するのは難しい。

信頼できる複数の機関が常にチェーンを確認し、ブロック情報が正しいかどうかを検証していれば、信頼できるチェーンであるか(正しいブロック/データが刻まれているか)がわかる。

そしてその情報はオープンであるため、必要であれば「信頼できる複数の機関」が本当に信頼できるかも含めて、自分で検証することができる。

そんな雰囲気でしょうか。

感想編

「ノードの外で」正しいかどうかを検証できる、というのは面白いな、と感じました。

普段、僕らが使っているサービスは隠れているものです。
なにかしら登録を行った場合、それがいつ登録されたかは定かではないものです。

例えば、保険の加入申請をして、それがいつだったか? という話になったとします。
証書でもあればいいかもしれませんが、なんらかの理由や事情で消滅することもあるかもしれません。
そうすると加入履歴は企業のサーバーに残っているだけだと思いますが、ただDBに記録されている場合、加入日を改変されていたとしても我々は確認する手段がないわけです。(あるかもしれないですが、とても難しい)

仮にこれがブロックチェーンを使用した場合、
最新ブロックまでのハッシュ値が導出できるか検証が可能であるため、途中で改変されることは現実的ではなくなります。
仮に途中を改変した場合、恐らくは最新ブロックまでのハッシュ値が導出できなくなるのかな、と。

“ブロックチェーンは改ざんが難しい”

という言葉は何度も見ましたし、「ノード同士が相互監視しているとかでそんな感じなんだろうな」なんてふわっと考えていました。
しかし今回の件で、「どうして改ざんが難しいのか?」という片りんに触れたのかな? と思いました。

最後に

速習Symbolも今回で最後のため内容とは関係ないのですが、ちょっと感想でも残しておこうと思います。

大変でした。誰だこのFlutterで書こうとか言い始めたの。最後、足りなかったじゃないか。ほんっとうにもう、、、言い始めたのは僕でしたね。

そもそもDart/Flutterも門外漢のため、本当に困ったんですよ。
どっかでも書いたかもしれないですが、僕は基本的に苦しまないと理解しないため、苦しんでやろうという思いがありました。

この速習Symbolも今年の3月の初めに記事をあげてますが、Flutterで速習Symbolを始めようと準備をしていたのは去年の8月? わかんないですけど、なんかそんな感じの時期だったんですよ。

元々はTypeScriptもJavaScriptもわかんないし、node.jsとかbrowselifyみたいな話とかも色々目にしたんですけど、全然わかんないし、まぁ、openapi3.ymlあたりと睨めっこしたらなんかできるんじゃないのかって。SDKの中身なんて読まないでがちゃがちゃとやってたんですけど。(openapi3.ymlも見方わからなくて困りましたけど)

openapi3.ymlの内容を手打ちであーだこーだ考えて、アカウントを作るにはEdなんとかかんとかが必要なのかーとか、ペイロードってやつを作るのかーとか、行き当たりばったりでやってました。
これがいけないんですけど、行き当たりばったりって楽しいですよね。仕事じゃなかったらですけど。

そんなわけですごく長くなりました。自分の書いたコードで各トランザクションを起こす、というのはなかなか楽しかったです。
ジェネレーターとかAIとかそういう流行りのものを使わずに全部手打ちでやっていたため、ペイロードがノードに受け付けられないということが起きるたびに禿げ上がりそうになったことも今ではいい思い出です。

来年は何か作れるといいですね。二番煎じ、三番煎じな気もしますが。

お疲れさまでした。ありがとうございました。

あ、最後に宣伝だけさせてください。

NCQSPV2RF5TTUMVE2TGTJ3XXAKI3FXW7WCBJHKI

もし頑張ったね! って↑に投げXYMしていただけると、今後の励みになります。

あと、Symbolノードの運用もしています。少額でも委任していただけると嬉しいです。

node.sixis.xyz

めちゃくちゃ赤字で評判です。メリットは。。。僕が嬉しいです。

以上です。ありがとうございました。

投稿者 和泉