4.トランザクション

ブロックチェーン上のデータ更新はトランザクションをネットワークにアナウンスすることによって行います。

4.1 トランザクションのライフサイクル

トランザクションを作成してから、改ざんが困難なデータとなるまでを順に説明します。

  • トランザクション作成
    • ブロックチェーンが受理できるフォーマットでトランザクションを作成します。
  • 署名
    • アカウントの秘密鍵でトランザクションを署名します。
  • アナウンス
    • 任意のノードに署名済みトランザクションを通知します。
  • 未承認トランザクション
    • ノードに受理されたトランザクションは、未承認トランザクションとして全ノードに伝播します
      • トランザクションに設定した最大手数料が、各ノード毎に設定されている最低手数料を満たさない場合はそのノードへは伝播しません。
  • 承認済みトランザクション
    • 約 30 秒に 1 度ごとに生成されるブロックに未承認トランザクションが取り込まれると、承認済みトランザクションとなります。
  • ロールバック
    • ノード間の合意に達することができずロールバックされたブロックに含まれていたトランザクションは、未承認トランザクションに差し戻されます。
      • 有効期限切れや、キャッシュからあふれたトランザクションは切り捨てられます。
  • ファイナライズ
    • 投票ノードによるファイナライズプロセスによりブロックが確定するとトランザクションはロールバック不可なデータとして扱うことができます。

ブロックとは

ブロックは約 30 秒ごとに生成され、高い手数料を支払ったトランザクションから優先に取り込まれ、ブロック単位で他のノードと同期します。 同期に失敗するとロールバックして、ネットワークが全体で合意が取れるまでこの作業を繰り返します。

4.2 トランザクション作成

まずは最も基本的な転送トランザクションを作成してみます。

Bob への転送トランザクション

送信先の Bob アドレスを作成しておきます。

bob = sym.Account.generateNewAccount(networkType);
console.log(bob.address);
> Address {address: 'TDWBA6L3CZ6VTZAZPAISL3RWM5VKMHM6J6IM3LY', networkType: 152}

トランザクションを作成します。

tx = sym.TransferTransaction.create(
  sym.Deadline.create(epochAdjustment), //Deadline:有効期限
  sym.Address.createFromRawAddress("TDWBA6L3CZ6VTZAZPAISL3RWM5VKMHM6J6IM3LY"),
  [],
  sym.PlainMessage.create("Hello Symbol!"), //メッセージ
  networkType //テストネット・メインネット区分
).setMaxFee(100); //手数料

各設定項目について説明します。

有効期限

sdk ではデフォルトで 2 時間後に設定されます。 最大 6 時間まで指定可能です。

sym.Deadline.create(epochAdjustment, 6);

メッセージ

トランザクションに最大 1023 バイトのメッセージを添付することができます。 バイナリデータであっても rawdata として送信することが可能です。

空メッセージ
sym.EmptyMessage;
平文メッセージ
sym.PlainMessage.create("Hello Symbol!");
暗号文メッセージ
sym.EncryptedMessage(
  "294C8979156C0D941270BAC191F7C689E93371EDBC36ADD8B920CF494012A97BA2D1A3759F9A6D55D5957E9D"
);

EncryptedMessage を使用すると、「指定したメッセージが暗号化されています」という意味のフラグ(目印)がつきます。 エクスプローラーやウォレットはそのフラグを参考にメッセージを無用にデコードしなかったり、非表示にしたりなどの処理を行います。 このメソッドが暗号化をするわけではありません。

生データ
sym.RawMessage.create(uint8Arrays[i]);

最大手数料

ネットワーク手数料については、常に少し多めに払っておけば問題はないのですが、最低限の知識は持っておく必要があります。 アカウントはトランザクションを作成するときに、ここまでは手数料として払ってもいいという最大手数料を指定します。 一方で、ノードはその時々で最も高い手数料となるトランザクションのみブロックにまとめて収穫しようとします。 つまり、多く払ってもいいというトランザクションが他に多く存在すると承認されるまでの時間が長くなります。 逆に、より少なく払いたいというトランザクションが多く存在し、その総額が大きい場合は、設定した最大額に満たない手数料額で送信が実現します。

トランザクションサイズ x feeMultipriler というもので決定されます。 176 バイトだった場合 maxFee を 100 で設定すると 17600μXYM = 0.0176XYM を手数料として支払うことを許容します。 feeMultiprier = 100 として指定する方法と maxFee = 17600 として指定する方法があります。

feeMultiprier = 100 として指定する方法
tx = sym.TransferTransaction.create(
  ,,,,
  networkType
).setMaxFee(100);
maxFee = 17600 として指定する方法
tx = sym.TransferTransaction.create(
  ,,,,
  networkType,
  sym.UInt64.fromUint(17600)
);

本書では以後、feeMultiprier = 100 として指定する方法で統一して説明します。

4.3 署名とアナウンス

作成したトランザクションを秘密鍵で署名して、任意のノードを通じてアナウンスします。

署名

signedTx = alice.sign(tx, generationHash);
console.log(signedTx);
出力例
> SignedTransaction
    hash: "3BD00B0AF24DE70C7F1763B3FD64983C9668A370CB96258768B715B117D703C2"
    networkType: 152
    payload:
"AE00000000000000CFC7A36C17060A937AFE1191BC7D77E33D81F3CC48DF9A0FFE892858DFC08C9911221543D687813ECE3D36836458D2569084298C09223F9899DF6ABD41028D0AD4933FC1E4C56F9DF9314E9E0533173E1AB727BDB2A04B59F048124E93BEFBD20000000001985441F843000000000000879E76C702000000986F4982FE77894ABC3EBFDC16DFD4A5C2C7BC05BFD44ECE0E000000000000000048656C6C6F2053796D626F6C21"
    signerPublicKey: "D4933FC1E4C56F9DF9314E9E0533173E1AB727BDB2A04B59F048124E93BEFBD2"
    type: 16724

トランザクションの署名には Account クラスと generationHash 値が必要です。

generationHash

  • テストネット
    • 7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836
  • メインネット
    • 57F7DA205008026C776CB6AED843393F04CD458E0AA2D9F1D5F31A402072B2D6

generationHash 値はそのブロックチェーンネットワークを一意に識別するための値です。 同じ秘密鍵をもつ他のネットワークに使いまわされないようにそのネットワーク個別のハッシュ値を織り交ぜて署名済みトランザクションを作成します。

アナウンス

res = await txRepo.announce(signedTx).toPromise();
console.log(res);
> TransactionAnnounceResponse {message: 'packet 9 was pushed to the network via /transactions'}

上記のスクリプトのように packet n was pushed to the network というレスポンスがあれば、トランザクションはノードに受理されたことになります。 これはトランザクションのフォーマット等に異常が無かった程度の意味しかありません。 Symbol ではノードの応答速度を極限に高めるため、トランザクションの内容を検証するまえに受信結果の応答を返し接続を切断します。 レスポンス値はこの情報を受け取ったにすぎません。フォーマットに異常があった場合は以下のようなメッセージ応答があります。

アナウンスに失敗した場合の応答例
Uncaught Error: {"statusCode":409,"statusMessage":"Unknown Error","body":"{\"code\":\"InvalidArgument\",\"message\":\"payload has an invalid format\"}"}

4.4 確認

ステータスの確認

ノードに受理されたトランザクションのステータスを確認

tsRepo = repo.createTransactionStatusRepository();
transactionStatus = await tsRepo
  .getTransactionStatus(signedTx.hash)
  .toPromise();
console.log(transactionStatus);
出力例
> TransactionStatus
    group: "confirmed"
    code: "Success"
    deadline: Deadline {adjustedValue: 11989512431}
    hash: "661360E61C37E156B0BE18E52C9F3ED1022DCE846A4609D72DF9FA8A5B667747"
    height: undefined

承認されると ` group: "confirmed"`となっています。

受理されたものの、エラーが発生していた場合は以下のような出力となります。トランザクションを書き直して再度アナウンスしてみてください。

> TransactionStatus
    group: "failed"
    code: "Failure_Core_Insufficient_Balance"
    deadline: Deadline {adjustedValue: 11990156766}
    hash: "A82507C6C46DF444E36AC94391EA2D0D7DD1A218948DED465A7A4F9D1B53CA0E"
    height: undefined

以下のように ResourceNotFound エラーが発生した場合はトランザクションが受理されていません。

Uncaught Error: {"statusCode":404,"statusMessage":"Unknown Error","body":"{\"code\":\"ResourceNotFound\",\"message\":\"no resource exists with id '18AEBC9866CD1C15270F18738D577CB1BD4B2DF3EFB28F270B528E3FE583F42D'\"}"}

考えられる可能性としては、トランザクションで指定した最大手数料が、ノードで設定された最低手数料に満たない場合や、 アグリゲートトランザクションとしてアナウンスすることが求められているトランザクションを単体のトランザクションでアナウンスした場合に発生するようです。

承認確認

トランザクションがブロックに承認されるまでに 30 秒程度かかります。

エクスプローラーで確認

signedTx.hash で取得できるハッシュ値を使ってエクスプローラーで検索してみましょう。

console.log(signedTx.hash);
> "661360E61C37E156B0BE18E52C9F3ED1022DCE846A4609D72DF9FA8A5B667747"
  • メインネット
    • https://symbol.fyi/transactions/661360E61C37E156B0BE18E52C9F3ED1022DCE846A4609D72DF9FA8A5B667747
  • テストネット
    • https://testnet.symbol.fyi/transactions/661360E61C37E156B0BE18E52C9F3ED1022DCE846A4609D72DF9FA8A5B667747

SDK で確認

txInfo = await txRepo
  .getTransaction(signedTx.hash, sym.TransactionGroup.Confirmed)
  .toPromise();
console.log(txInfo);
出力例
> TransferTransaction
    deadline: Deadline {adjustedValue: 12883929118}
    maxFee: UInt64 {lower: 17400, higher: 0}
    message: PlainMessage {type: 0, payload: 'Hello Symbol!'}
    mosaics: []
    networkType: 152
    payloadSize: 174
    recipientAddress: Address {address: 'TDWBA6L3CZ6VTZAZPAISL3RWM5VKMHM6J6IM3LY', networkType: 152}
    signature: "7A3562DCD7FEE4EE9CB456E48EFEEC687647119DC053DE63581FD46CA9D16A829FA421B39179AABBF4DE0C1D987B58490E3F95C37327358E6E461832E3B3A60D"
    signer: PublicAccount {publicKey: '0E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D26', address: Address}
  > transactionInfo: TransactionInfo
        hash: "DA4B672E68E6561EAE560FB89B144AFE1EF75D2BE0D9B6755D90388F8BCC4709"
        height: UInt64 {lower: 330012, higher: 0}
        id: "626413050A21EB5CD286E17D"
        index: 1
        merkleComponentHash: "DA4B672E68E6561EAE560FB89B144AFE1EF75D2BE0D9B6755D90388F8BCC4709"
    type: 16724
    version: 1
注意点

トランザクションはブロックで承認されたとしても、ロールバックが発生するとトランザクションの承認が取り消される場合があります。 ブロックが承認された後、数ブロックの承認が進むと、ロールバックの発生する確率は減少していきます。 また、Voting ノードの投票で実施されるファイナライズブロックを待つことで、記録されたデータは確実なものとなります。

スクリプト例

トランザクションをアナウンスした後は以下のようなスクリプトを流すと、チェーンの状態を把握しやすくて便利です。

hash = signedTx.hash;
tsRepo = repo.createTransactionStatusRepository();
transactionStatus = await tsRepo.getTransactionStatus(hash).toPromise();
console.log(transactionStatus);
txInfo = await txRepo
  .getTransaction(hash, sym.TransactionGroup.Confirmed)
  .toPromise();
console.log(txInfo);

4.5 トランザクション履歴

Alice が送受信したトランザクション履歴を一覧で取得します。

result = await txRepo
  .search({
    group: sym.TransactionGroup.Confirmed,
    embedded: true,
    address: alice.address,
  })
  .toPromise();

txes = result.data;
txes.forEach((tx) => {
  console.log(tx);
});
出力例
> TransferTransaction
    type: 16724
    networkType: 152
    payloadSize: 176
    deadline: Deadline {adjustedValue: 11905303680}
    maxFee: UInt64 {lower: 200000000, higher: 0}
    recipientAddress: Address {address: 'TBXUTAX6O6EUVPB6X7OBNX6UUXBMPPAFX7KE5TQ', networkType: 152}
    signature: "E5924A1EB653240A7220405A4DD4E221E71E43327B3BA691D267326FEE3F57458E8721907188DB33A3F2A9CB1D0293845B4D0F1D7A93C8A3389262D1603C7108"
    signer: PublicAccount {publicKey: 'BDFAF3B090270920A30460AA943F9D8D4FCFF6741C2CB58798DBF7A2ED6B75AB', address: Address}
  > message: RawMessage
      payload: ""
      type: -1
  > mosaics: Array(1)
      0: Mosaic
        amount: UInt64 {lower: 10000000, higher: 0}
        id: MosaicId
          id: Id {lower: 760461000, higher: 981735131}
  > transactionInfo: TransactionInfo
      hash: "308472D34BE1A58B15A83B9684278010F2D69B59E39127518BE38A4D22EEF31D"
      height: UInt64 {lower: 301717, higher: 0}
      id: "6255242053E0E706653116F9"
      index: 0
      merkleComponentHash: "308472D34BE1A58B15A83B9684278010F2D69B59E39127518BE38A4D22EEF31D"

TransactionType は以下の通りです。

{0: 'RESERVED', 16705: 'AGGREGATE_COMPLETE', 16707: 'VOTING_KEY_LINK', 16708: 'ACCOUNT_METADATA', 16712: 'HASH_LOCK', 16716: 'ACCOUNT_KEY_LINK', 16717: 'MOSAIC_DEFINITION', 16718: 'NAMESPACE_REGISTRATION', 16720: 'ACCOUNT_ADDRESS_RESTRICTION', 16721: 'MOSAIC_GLOBAL_RESTRICTION', 16722: 'SECRET_LOCK', 16724: 'TRANSFER', 16725: 'MULTISIG_ACCOUNT_MODIFICATION', 16961: 'AGGREGATE_BONDED', 16963: 'VRF_KEY_LINK', 16964: 'MOSAIC_METADATA', 16972: 'NODE_KEY_LINK', 16973: 'MOSAIC_SUPPLY_CHANGE', 16974: 'ADDRESS_ALIAS', 16976: 'ACCOUNT_MOSAIC_RESTRICTION', 16977: 'MOSAIC_ADDRESS_RESTRICTION', 16978: 'SECRET_PROOF', 17220: 'NAMESPACE_METADATA', 17229: 'MOSAIC_SUPPLY_REVOCATION', 17230: 'MOSAIC_ALIAS', 17232: 'ACCOUNT_OPERATION_RESTRICTION'

MessageType は以下の通りです。

{0: 'PlainMessage', 1: 'EncryptedMessage', 254: 'PersistentHarvestingDelegationMessage', -1: 'RawMessage'}

4.6 アグリゲートトランザクション

Symbol では複数のトランザクションを 1 ブロックにまとめてアナウンスすることができます。 最大で 100 件のトランザクションをまとめることができます(連署者が異なる場合は 25 アカウントまでを連署指定可能)。 以降の章で扱う内容にアグリゲートトランザクションへの理解が必要な機能が含まれますので、 本章ではアグリゲートトランザクションのうち、簡単なものだけを紹介します。

起案者の署名だけが必要な場合

bob = sym.Account.generateNewAccount(networkType);
carol = sym.Account.generateNewAccount(networkType);

innerTx1 = sym.TransferTransaction.create(
  undefined, //Deadline
  bob.address, //送信先
  [],
  sym.PlainMessage.create("tx1"),
  networkType
);

innerTx2 = sym.TransferTransaction.create(
  undefined, //Deadline
  carol.address, //送信先
  [],
  sym.PlainMessage.create("tx2"),
  networkType
);

aggregateTx = sym.AggregateTransaction.createComplete(
  sym.Deadline.create(epochAdjustment),
  [
    innerTx1.toAggregate(alice.publicAccount), //送信元アカウントの公開鍵
    innerTx2.toAggregate(alice.publicAccount), //送信元アカウントの公開鍵
  ],
  networkType,
  [],
  sym.UInt64.fromUint(1000000)
);
signedTx = alice.sign(aggregateTx, generationHash);
await txRepo.announce(signedTx).toPromise();

まず、アグリゲートトランザクションに含めるトランザクションを作成します。 このとき Deadline を指定する必要はありません。 リスト化するときに、生成したトランザクションに toAggregate を追加して送信元アカウントの公開鍵を指定します。 ちなみに送信元アカウントと署名アカウントが 必ずしも一致するとは限りません 。 後の章での解説で「Bob の送信トランザクションを Alice が署名する」といった事が起こり得るためこのような書き方をします。 これは Symbol ブロックチェーンでトランザクションを扱ううえで最も重要な概念になります。 なお、本章で扱うトランザクションは同じ Alice ですので、アグリゲートボンデッドトランザクションへの署名も Alice を指定します。

4.7 現場で使えるヒント

存在証明

アカウントの章でアカウントによるデータの署名と検証する方法について説明しました。 このデータをトランザクションに載せてブロックチェーンが承認することで、 アカウントがある時刻にあるデータの存在を認知したことを消すことができなくなります。 タイムスタンプの刻印された電子署名を利害関係者間で所有することと同じ意味があると考えることもできます。 (法律的な判断は他の方にお任せします)

ブロックチェーンは、この消せない「アカウントが認知したという事実」の存在をもって送信などのデータ更新を行います。 また、誰もがまだ知らないはずの事実を知っていたことの証明としてブロックチェーンを利用することもできます。 ここでは、その存在が証明されたデータをトランザクションに載せる2つの方法について説明します。

デジタルデータのハッシュ値(SHA256)出力方法

ファイルの要約値をブロックチェーンに記録することでそのファイルの存在を証明することができます。

各 OS ごとのファイルの SHA256 でハッシュ値を計算する方法は下記の通りです。

#Windows
certutil -hashfile WINファイルパス SHA256
#Mac
shasum -a 256 MACファイルパス
#Linux
sha256sum Linuxファイルパス

大きなデータの分割

トランザクションのペイロードには 1023 バイトしか格納できないため、 大きなデータは分割してペイロードに詰め込んでアグリゲートトランザクションにします。

bigdata =
  "C00200000000000093B0B985101C1BDD1BC2BF30D72F35E34265B3F381ECA464733E147A4F0A6B9353547E2E08189EF37E50D271BEB5F09B81CE5816BB34A153D2268520AF630A0A0E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D26000000000198414140770200000000002A769FB40000000076B455CFAE2CCDA9C282BF8556D3E9C9C0DE18B0CBE6660ACCF86EB54AC51B33B001000000000000DB000000000000000E5C72B0D5946C1EFEE7E5317C5985F106B739BB0BC07E4F9A288417B3CD6D26000000000198544198205C1A4CE06C45B3A896B1B2360E03633B9F36BF7F22338B000000000000000066653465353435393833444430383935303645394533424446434235313637433046394232384135344536463032413837364535303734423641303337414643414233303344383841303630353343353345354235413835323835443639434132364235343233343032364244444331443133343139464435353438323930334242453038423832304100000000006800000000000000B2D4FD84B2B63A96AA37C35FC6E0A2341CEC1FD19C8FFC8D93CCCA2B028D1E9D000000000198444198205C1A4CE06C45B3A896B1B2360E03633B9F36BF7F2233BC089179EBBE01A81400140035383435344434373631364336433635373237396800000000000000B2D4FD84B2B63A96AA37C35FC6E0A2341CEC1FD19C8FFC8D93CCCA2B028D1E9D000000000198444198205C1A4CE06C45B3A896B1B2360E03633B9F36BF7F223345ECB996EDDB9BEB1400140035383435344434373631364336433635373237390000000000000000B2D4FD84B2B63A96AA37C35FC6E0A2341CEC1FD19C8FFC8D93CCCA2B028D1E9D5A71EBA9C924EFA146897BE6C9BB3DACEFA26A07D687AC4A83C9B03087640E2D1DDAE952E9DDBC33312E2C8D021B4CC0435852C0756B1EBD983FCE221A981D02";

let payloads = [];
for (let i = 0; i < bigdata.length / 1023; i++) {
  payloads.push(bigdata.substr(i * 1023, 1023));
}
console.log(payloads);