4.トランザクション
ブロックチェーン上のデータ更新はトランザクションをネットワークにアナウンスすることによって行います。
4.1 トランザクションのライフサイクル
トランザクションを作成してから、改ざんが困難なデータとなるまでを順に説明します。
- トランザクション作成
- ブロックチェーンが受理できるフォーマットでトランザクションを作成します。
- 署名
- アカウントの秘密鍵でトランザクションを署名します。
- アナウンス
- 任意のノードに署名済みトランザクションを通知します。
- 未承認トラ ンザクション
- ノードに受理されたトランザクションは、未承認トランザクションとして全ノードに伝播します
- トランザクションに設定した最大手数料が、各ノード毎に設定されている最低手数料を満たさない場合はそのノードへは伝播しません。
- ノードに受理されたトランザクションは、未承認トランザクションとして全ノードに伝播します
- 承認済みトランザクション
- 約30秒に1度ごとに生成されるブロックに未承認トランザクションが取り込まれると、承認済みトランザクションとなります。
- ロールバック
- ノード間の合意に達することができずロールバックされたブロックに含まれていたトランザクションは、未承認トランザクションに差し戻されます。
- 有効期限切れや、キャッシュからあふれたトランザクションは切り捨てられます。
- ノード間の合意に達することができずロールバックされたブロックに含まれていたトランザクションは、未承認トランザクションに差し戻されます。
- ファイナライズ
- 投票ノードによるファイナライズプロセスによりブロックが確定するとトランザクションはロールバック不可なデータとして扱うことができます。
ブロックとは
ブロックは約30秒ごとに生成され、高い手数料を支払ったトランザクションから優先に取り込まれ、ブロック単位で他のノードと同期します。 同期に失敗するとロールバックして、ネットワークが全体で合意が取れるまでこの作業を繰り返します。
4.2 トランザクション作成
まずは最も基本的な転送トランザクションを作成してみます。
Bobへの転送トランザクション
送信先のBobアドレスを作成しておきます。
bobKey = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
bobAddress = facade.network.publicKeyToAddress(bobKey.publicKey);
console.log(bobAddress.toString());
> TDWBA6L3CZ6VTZAZPAISL3RWM5VKMHM6J6IM3LY
トランザクションを作成します。
messageData = new Uint8Array([
0x00,
...new TextEncoder("utf-8").encode("Hello, Symbol!"),
]); // 平文メッセージ
tx = facade.transactionFactory.create({
type: "transfer_transaction_v1", // Txタイプ:転送Tx
signerPublicKey: aliceKey.publicKey, // 署名者公開鍵
deadline: facade.network.fromDatetime(Date.now()).addHours(2).timestamp, //Deadline:有効期限
recipientAddress: bobAddress.toString(),
mosaics: [
// { mosaicId: 0x72C0212E67A08BCEn, amount: 1000000n } // 1XYM送金
],
message: messageData,
});
tx.fee = new symbolSdk.symbol.Amount(BigInt(tx.size * 100)); //手数料
console.log(tx);
各設定項目について説明します。
有効期限
sdkではデフォルトで2時間後に設定されます。 最大6時間まで指定可能です。
facade.network.fromDatetime(Date.now()).addHours(6).timestamp;
メッセージ
トランザクションに最大1023バイトのメッセージを添付することができます。 バイナリデータであってもrawdataとして送信することが可能です。
空メッセージ
messageData = new Uint8Array();
平文メッセージ
v3 では先頭に平文メッセージを表すメッセージタイプ 0x00
を付加する必要があります。
messageData = new Uint8Array([
0x00,
...new TextEncoder("utf-8").encode("Hello, Symbol!"),
]);
暗号文メッセージ
MessageEncoder
を使用して暗号化すると、自動で暗号文メッセージを表すメッセージタイプ 0x01
が付加されます。
message = "Hello Symbol!";
aliceMsgEncoder = new symbolSdk.symbol.MessageEncoder(aliceKey);
messageData = aliceMsgEncoder.encode(
bobKey.publicKey,
new TextEncoder().encode(message),
);
生データ
v3 では先頭に生データを表すメッセージタイプ 0xFF
を付加する必要があります。
messageData = new Uint8Array([
0xff,
...new TextEncoder("utf-8").encode("Hello, Symbol!"),
]);
最大手数料
ネットワーク手数料については、常に少し多めに払っておけば問題はないのですが、最低限の知識は持っておく必要があります。 アカウントはトランザクションを作成するときに、ここまでは手数料として払ってもいいという最大手数料を指定します。 一方で、ノードはその時々で最も高い手数料となるトランザクションのみブロックにまとめて収穫しようとします。 つまり、多く払ってもいいというトランザクションが他に多く存在すると承認されるまでの時間が長くなります。 逆に、より少なく払いたいというトランザクションが多く存在し、その総額が大きい場合は、設定した最大額に満たない手数料額で送信が実現します。
トランザクションサイズ x feeMultiprilerというもので決定されます。 176バイトだった場合 maxFee を100で設定すると 17600μXYM = 0.0176XYMを手数料として支払うことを許容します。 feeMultiprier = 100として指定する方法とmaxFee = 17600 として指定する方法があります。
feeMultiprier = 100として指定する方法
tx = facade.transactionFactory.create({
type: "transfer_transaction_v1", // Txタイプ:転送Tx
// 省略
});
tx.fee = new symbolSdk.symbol.Amount(BigInt(tx.size * 100)); //手数料
maxFee = 17600 として指定する方法
tx = facade.transactionFactory.create({
type: "transfer_transaction_v1", // Txタイプ:転送Tx
fee: 17600n, // 手数料
// 省略
});
本書では以後、feeMultiprier = 100として指定する方法で統一して説明します。
4.3 署名とアナウンス
作成したトランザクションを秘密鍵で署名して、任意のノードを通じてアナウンスします。
署名
sig = facade.signTransaction(aliceKey, tx);
jsonPayload = facade.transactionFactory.constructor.attachSignature(tx, sig);
出力例
> '{"payload": "AF0000000000000041EE9F8B3EB4D54F069B1CD47A79656DCE4C85B486D1735DF054B91838ECF6E06B6F371BB986E676A5F5BF091A5DEF5230BC6E6112F7D2104BE24923355B890869A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB00000000019854415C44000000000000426B07250500000098A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329D0F00000000000000FF48656C6C6F2C2053796D626F6C21"}'
アナウンス
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);
> {message: 'packet 9 was pushed to the network via /transactions'}
上記のスクリプトのように packet n was pushed to the network
というレスポンスがあれば、トランザクションはノードに受理されたことになります。
これはトランザクションのフォーマット等に異常が無かった程度の意味しかありません。
Symbolではノードの応答速度を極限に高めるため、トランザクションの内容を検証するまえに受信結果の応答を返し接続を切断します。
レスポンス値はこの情報を受け取ったにすぎません。フォーマットに異常があった場合は以下のようなメッセージ応答があります。
アナウンスに失敗した場合の応答例
> {code: 'InvalidArgument', message: 'payload has an invalid format'}
4.4 確認
ステータスの確認
ノードに受理されたトランザクションのステータスを確認
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]);
}
出力例
> {group: 'confirmed', code: 'Success', hash: '02DC42E10B3E51B49AA9CD1074481C4B0764D8DA4BE7F33F83DC1DC9ED84C79D', deadline: '22164976452', height: '636865'}
承認されると 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(facade.hashTransaction(tx).toString());
> "661360E61C37E156B0BE18E52C9F3ED1022DCE846A4609D72DF9FA8A5B667747"
- メインネット
- テストネット
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);
出力例
> {meta: {…}, transaction: {…}, id: '64B253382F7CE156B01031C1'}
id: "64B253382F7CE156B01031C1"
> meta:
feeMultiplier: 100
hash: "02DC42E10B3E51B49AA9CD1074481C4B0764D8DA4BE7F33F83DC1DC9ED84C79D"
height: "636865"
index: 0
merkleComponentHash: "02DC42E10B3E51B49AA9CD1074481C4B0764D8DA4BE7F33F83DC1DC9ED84C79D"
timestamp: "22157844926"
> transaction:
deadline: "22164976452"
maxFee: "17500"
message: "0048656C6C6F2C2053796D626F6C21"
mosaics: []
network: 152
recipientAddress: "98A8D76FEF8382274D472EE377F2FF3393E5B62C08B4329D"
signature: "29783E718E414D2A2B62821FC53E66DCFEBDC1EF10FFD35F7662263FA7332F01CD47676429A982A85FEC7F2E7983D36F4BCE5C5AB1BB4A591BC54979AF906D0A"
signerPublicKey: "69A31A837EB7DE323F08CA52495A57BA0A95B52D1BB54CEA9A94C12A87B1CADB"
size: 175
type: 16724
version: 1
注意点
トランザクションはブロックで承認されたとしても、ロールバックが発生するとトランザクションの承認が取り消される場合があります。 ブロックが承認された後、数ブロックの承認が進むと、ロールバックの発生する確率は減少していきます。 また、Votingノードの投票で実施されるファイナライズブロックを待つことで、記録されたデータは確実なものとなります。
スクリプト例
トランザクションをアナウンスした後は以下のようなスクリプトを流すと、チェーンの状態を把握しやすくて便利です。
hash = facade.hashTransaction(tx).toString();
// ステータスの確認
body = {
hashes: hash,
};
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]);
}
console.log(transactionStatus);
// 承認確認
txInfo = await fetch(new URL("/transactions/confirmed/" + hash, NODE), {
method: "GET",
headers: { "Content-Type": "application/json" },
})
.then((res) => res.json())
.then((json) => {
return json;
});
console.log(txInfo);
4.5トランザクション履歴
Aliceが送受信したトランザクション履歴を一覧で取得します。
params = new URLSearchParams({
address: aliceAddress.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);
});
出力例
// 取得できたTx数分表示されます
> {meta: {…}, transaction: {…}, id: '636EF1EF8EFA5A777403DEDA'}
id: "636EF1EF8EFA5A777403DEDA"
> meta:
feeMultiplier: 1136363
hash: "EFDD7A4D419CAC459DA034089DF7F25E416A4782A1A6D9DBEC4ECF9AC3AAF1A9"
height: "18925"
index: 0
merkleComponentHash: "EFDD7A4D419CAC459DA034089DF7F25E416A4782A1A6D9DBEC4ECF9AC3AAF1A9"
timestamp: "964811467"
> transaction:
deadline: "971971235"
maxFee: "200000000"
> mosaics: Array(1)
0:
amount: "100000000"
id: "72C0212E67A08BCE"
network: 152
recipientAddress: "982B2AA2295B5C23528ADDEE7F29F6521944E9F2340428AB"
signature: "17104B0B8C9CB34C4FC42E448488A6608387841715921160F81C0FD60054CFACAE7200E3F41D911AA914D431197D923D6D98566E711DDC13C4BF0C768AAB3A04"
signerPublicKey: "81EA7C15E7EC06261C9F654F54EAC4748CFCF00E09A8FE47779ACD14A7602004"
size: 176
type: 16724
version: 1
transaction.type
は v2 の TransactionType
と同じです。
4.5.1 Txペイロード作成時のメッセージの差異
v3 ではメッセージはバイナリデータで表されます。 平文メッセージはバイナリデータに変換され、暗号化メッセージや生データのようなバイナリデータはそのままのバイナリデータとなります。
message = "Hello Symbol!";
plainMessage = new Uint8Array([
0x00,
...new TextEncoder("utf-8").encode("Hello, Symbol!"),
]);
console.log(plainMessage);
aliceMsgEncoder = new symbolSdk.symbol.MessageEncoder(aliceKey);
encryptedMessage = aliceMsgEncoder.encode(
bobKey.publicKey,
new TextEncoder().encode(message),
);
console.log(encryptedMessage);
rawMessage = new Uint8Array([0xff, 0x10, 0x20, 0x30]);
console.log(rawMessage);
出力例
> Uint8Array(15) [...]
0: 0 // メッセージタイプ 0x00 (平文メッセージ)
1: 72 // 'H'
2: 101 // 'e'
3: 108 // 'l'
// 以下省略
> Uint8Array(42) [...]
0: 1 // メッセージタイプ 0x01 (暗号化メッセージ)
1: 163
2: 55
3: 105
// 以下省略
> Uint8Array(4) [...]
0: 255 // メッセージタイプ 0xFF (生データ)
1: 16 // 0x10
2: 32 // 0x20
3: 48 // 0x30
署名時にTxのペイロードを作成する際には、メッセージデータを16進数文字列に変換します。 v2 では文字列データ、 v3 ではバイナリデータがメッセージデータとなりますが、 v2 における暗号化メッセージや生データは16進数文字列に変換されたものですので、元のデータから2回変換されることとなります。 また、ブロックチェーン上のTxを取得して表示する際も同様に、 v2 では16進数文字列から戻すための変換が2回行われます。
このようなメッセージデータの変換方法の違いから、Txを作成したSDKとバージョンが異なるSDKで読み込む際に正しく読み込まれない場合があります。