Skip to main content
Version: 2.0.0

Transaction deserialization example

**PLEASE NOTE** In this document long snippets representing hex-encoded bytes are split into 64 character lines. Putting such a text in multiple lines does not mean that the newline character is part of the binary payload - newlines are used purely as formatting.

In this document we will go through byte-deserialization of a full Transaction example. We will assume an examble binary payload and we will step-by-step deserialize it. The goal here is to show a real-life example of how calltable serialization works. To achieve that we will have a Transaction::Version1 variant since Transaction::Deploy doesn't use calltable serialization.

Here is an example hex-encoded byte array payload:

0x01030000000000000000000100200000000200170100007d01000013891c67
a803d1803c932c6a9c342a44e8adb7be3f287cd69a8cab8b2a446fa606000000
00000000000001003600000002003e0000000300460000000400520000000500
7d000000cb00000002000000000000000000010001000000220000000001d9bf
2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c7b00
00000000000080ee360000000000080000006d792d636861696e040000000000
0000000001000100000002000900000003000a0000000b0000000053aa080000
000000010104000000000005000000000000000001000f000000010000000000
00000000010000000002000f0000000100000000000000000001000000050300
0f0000000100000000000000000001000000000100000001d9bf2148748a85c8
9da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c013104d575cdd9a8
65a54eae377586dd2ea9912fde9511d3471f63a73c1162b7f6c7a290611a527f
c14247e4eb86c308c27f45d2ece4abc506291c600c54ee720b

Deserializing Transaction

The first byte of the payload is 01, by which we know that we need to attempt to treat it as a Transaction::Version1. We will drop the first byte and proceed deserializing the rest as TransactionV1 - since that is the internal payload of Transaction::Version1

Deserializing TransactionV1

After unpacking the discriminator byte for Transaction we are left with the following bytes:

0x030000000000000000000100200000000200170100007d01000013891c67a8
03d1803c932c6a9c342a44e8adb7be3f287cd69a8cab8b2a446fa60600000000
000000000001003600000002003e00000003004600000004005200000005007d
000000cb00000002000000000000000000010001000000220000000001d9bf21
48748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c7b0000
000000000080ee360000000000080000006d792d636861696e04000000000000
00000001000100000002000900000003000a0000000b0000000053aa08000000
0000010104000000000005000000000000000001000f00000001000000000000
000000010000000002000f00000001000000000000000000010000000503000f
0000000100000000000000000001000000000100000001d9bf2148748a85c89d
a5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c013104d575cdd9a865
a54eae377586dd2ea9912fde9511d3471f63a73c1162b7f6c7a290611a527fc1
4247e4eb86c308c27f45d2ece4abc506291c600c54ee720b

TransactionV1 is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x03000000le-encoded number of entries in the calltable header for TransactionV1
0x0000le encoded index of the first calltable entry: 0 means hash
0x00000000le encoded offset of the first calltable entry
0x0100le encoded index of the second calltable entry: 1 means payload
0x20000000le encoded offset of the second calltable entry: 32 means that the bytes of payload start at byte 32 of the binary payload of the calltable envelope
0x0200le encoded index of the third calltable entry: 2 means approvals
0x17010000le encoded offset of the third calltable entry: 279 means that the bytes of payload start at byte 279 of the binary payload of the calltable envelope

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0x7d010000381 (4-bytes unsigned LE) - number of bytes in the paylod
0x13891c67a803d1803c932c6a9c342a44e8adb7be3f287cd69a8cab8b2a446fa6hash field of type Digest, should be interpreted as here
0x0600000000000000000001003600000002003e000000030046000000040052
00000005007d000000cb00000002000000000000000000010001000000220000
000001d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0a
e2900c7b0000000000000080ee360000000000080000006d792d636861696e04
00000000000000000001000100000002000900000003000a0000000b00000000
53aa080000000000010104000000000005000000000000000001000f00000001
000000000000000000010000000002000f000000010000000000000000000100
00000503000f000000010000000000000000000100000000
binary representation of payload deserialization drill-down here
0x0100000001d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536
354f0ae2900c013104d575cdd9a865a54eae377586dd2ea9912fde9511d3471f
63a73c1162b7f6c7a290611a527fc14247e4eb86c308c27f45d2ece4abc50629
1c600c54ee720b
binary representation of a collection Approvals

payload field deserialization

Previously we established that the payload fields raw bytes are:

0x0600000000000000000001003600000002003e000000030046000000040052
00000005007d000000cb00000002000000000000000000010001000000220000
000001d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0a
e2900c7b0000000000000080ee360000000000080000006d792d636861696e04
00000000000000000001000100000002000900000003000a0000000b00000000
53aa080000000000010104000000000005000000000000000001000f00000001
000000000000000000010000000002000f000000010000000000000000000100
00000503000f000000010000000000000000000100000000

now we will attempt to deserialize it as an instance of TransactionV1Payload.

TransactionV1Payload is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x06000000le-encoded number of entries in the calltable header for TransactionV1Payload
0x0000le encoded index of the first calltable entry: 0 means initiator_addr
0x00000000le encoded offset of the first calltable entry - means that the initiator_addr starts at index 0 of the envelope payload
0x0100le encoded index of the second calltable entry: 1 means timestamp
0x36000000le encoded offset of the second calltable entry - means that the timestamp starts at index 54 of the envelope payload
0x0200le encoded index of the third calltable entry: 2 means ttl
0x3e000000le encoded offset of the third calltable entry - means that the ttl starts at index 62 of the envelope payload
0x0300le encoded index of the fourth calltable entry: 3 means chain_name
0x46000000le encoded offset of the fourth calltable entry - means that the chain_name starts at index 70 of the envelope payload
0x0400le encoded index of the fifth calltable entry: 4 means pricing_mode
0x52000000le encoded offset of the fifth calltable entry - means that the pricing_mode starts at index 82 of the envelope payload
0x0500le encoded index of the sixth calltable entry: 5 means fields
0x7d000000le encoded offset of the sixth calltable entry - means that the fields starts at index 125 of the envelope payload

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0xcb000000 203 (4-bytes unsigned LE) - number of bytes in the paylod
0x02000000000000000000010001000000220000000001d9bf2148748a85c89d
a5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c
bytes which should be interpreted as InitiatorAddr
0x7b00000000000000bytes which should be interpreted as Timestamp (le unsigned 8 bytes unix-style milliseconds value which translates to 123)
0x80ee360000000000bytes which should be interpreted as TTL (le unsigned 8 bytes value which translates to 3600000)
0x080000006d792d636861696ebytes which should be interpreted as String
0x0400000000000000000001000100000002000900000003000a0000000b0000
000053aa0800000000000101
bytes which should be interpreted as PricingMode
0x04000000000005000000000000000001000f00000001000000000000000000
010000000002000f00000001000000000000000000010000000503000f000000
010000000000000000000100000000
bytes which should be interpreted as a map of field id -> field mapping. Please see this paragraph for details

payload.initiator_addr field deserialization

Previously we established that the initiator_addr fields raw bytes were:

0x02000000000000000000010001000000220000000001d9bf2148748a85c89
da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2900c

now we will attempt to deserialize it as an instance of InitiatorAddr.

InitiatorAddr is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x02000000le-encoded number of entries in the calltable header for InitiatorAddr
0x0000le encoded index of the first calltable entry: 0 means variant discriminator of the union type (since InitiatorAddr is a union type)
0x00000000le encoded offset of the first calltable entry - means that the variant discriminator starts at byte 0 of the envelope payload.
0x0100le encoded index of the second calltable entry: 1 - we don't really know what field it is at this moment since we don't know what the variant is
0x01000000le encoded offset of the second calltable entry - means that the payload of the first field of the strucure starts at position 1 of the envelope payload

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0x2200000034 (4-bytes unsigned LE) - number of bytes in the paylod
0x00value of the discriminator - deserializes to PublicKey variant. We now know that index = 1 from the header points to an instance of PublicKey
0x01d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536354f0ae2
900c
PublicKey whish should be interpreted as explained here

payload.pricing_mode field deserialization

Previously we established that the pricing_mode fields raw bytes were:

0x0400000000000000000001000100000002000900000003000a0000000b000
0000053aa0800000000000101

now we will attempt to deserialize it as an instance of PricingMode.

PricingMode is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x04000000le-encoded number of entries in the calltable header for PricingMode
0x0000le encoded index of the first calltable entry: 0 means variant discriminator of the union type (since PricingMode is a union type)
0x00000000le encoded offset of the first calltable entry - means that the variant discriminator starts at byte 0 of the envelope payload.
0x0100le encoded index of the second calltable entry: 1 - we don't really know what field it is at this moment since we don't know what the variant is
0x01000000le encoded offset of the second calltable entry - means that the payload of the first field of the strucure starts at index 1 of the envelope payload
0x0200le encoded index of the third calltable entry: 2 - we don't really know what field it is at this moment since we don't know what the variant is
0x09000000le encoded offset of the third calltable entry - means that the payload of the second field of the strucure starts at index 9 of the envelope payload
0x0300le encoded index of the fourth calltable entry: 3 - we don't really know what field it is at this moment since we don't know what the variant is
0x0a000000le encoded offset of the fourth calltable entry - means that the payload of the second field of the strucure starts at index 10 of the envelope payload

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0x0b00000011 (4-bytes unsigned LE) - number of bytes in the paylod
0x00value of the discriminator - deserializes to PaymentLimited variant. We now know that: index = 1 from the header points to a 8 bytes unsigned number payment_amount field; index = 2 from the header points to a 1 byte unsigned number gas_price_tolerance field; index = 3 from the header points to a 1 byte bool standard_payment field
0x53aa080000000000567891 (8-bytes unsigned LE payment_amount )
0x011 (1-bytes unsigned LE) gas_price_tolerance
0x01true standard_payment

payload.fields field deserialization

Previously we established that the fields fields raw bytes were:

0x04000000000005000000000000000001000f00000001000000000000000000
010000000002000f00000001000000000000000000010000000503000f000000
010000000000000000000100000000

Payload of this field is not serialized using calltable. It is serialized as a map (for details, see here). We will attempt to deconstruct the payload:

hex formatted bytesInterpretation
0x040000004 - number of entries in the map (4 bytes LE encoded unsigned number)
0x00000 - key of first entry (2 bytes encoded unsigned number)
0x050000005 - number of bytes of the payload of the value under key 0 (4 bytes encoded unsigned number)
0x00000000005 bytes read which is the raw payload for key 0. Based on the fields index table here we see that it should be interpreted as TransactionArgs. To deserialize TransactionArgs see here
0x01001 - key of second entry (2 bytes encoded unsigned number)
0x0f00000015 - number of bytes of the payload of the value under key 1 (4 bytes encoded unsigned number)
0x01000000000000000000010000000015 bytes read which is the raw payload for key 1. Based on the fields index table here we see that it should be interpreted as TransactionTarget. Deserialization explanation is here
0x02002 - key of third entry (2 bytes encoded unsigned number)
0x0f00000015 - number of bytes of the payload of the value under key 2 (4 bytes encoded unsigned number)
0x01000000000000000000010000000515 bytes read which is the raw payload for key 2. Based on the fields index table here we see that it should be interpreted as TransactionEntryPoint. Deserialization explanation is here
0x03003 - key of third entry (2 bytes encoded unsigned number)
0x0f00000015 - number of bytes of the payload of the value under key 2 (4 bytes encoded unsigned number)
0x01000000000000000000010000000015 bytes read which is the raw payload for key 2. Based on the fields index table here we see that it should be interpreted as TransactionScheduling. Deserialization explanation is here

payload.fields.1 field deserialization

Previously we established that the TransactionTarget fields map entry raw bytes are:

0x010000000000000000000100000000

TransactionTarget is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x01000000le-encoded number of entries in the calltable header for TransactionTarget
0x0000le encoded index of the first calltable entry: 0 means variant discriminator of the union type (since PricingMode is a union type)
0x00000000le encoded offset of the first calltable entry - means that the variant discriminator starts at byte 0 of the envelope payload.

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0x010000001 (4-bytes unsigned LE) - number of bytes in the paylod
0x000 value of the discriminator - deserializes to Native variant. This variant has no fields

payload.fields.2 field deserialization

Previously we established that the TransactionEntryPoint fields map entry raw bytes are:

0x010000000000000000000100000005

TransactionEntryPoint is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x01000000le-encoded number of entries in the calltable header for TransactionEntryPoint
0x0000le encoded index of the first calltable entry: 0 means variant discriminator of the union type (since TransactionEntryPoint is a union type)
0x00000000le encoded offset of the first calltable entry - means that the variant discriminator starts at byte 0 of the envelope payload.

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0x010000001 (4-bytes unsigned LE) - number of bytes in the paylod
0x055 value of the discriminator - deserializes to Delegate variant. This variant has no fields

payload.fields.3 field deserialization

Previously we established that the TransactionScheduling fields map entry raw bytes are:

0x010000000000000000000100000000

TransactionScheduling is serialized using calltable representation. From the calltable serialization document we know that we need to split the bytes into a header and payload:

Interpretation of the bytes representing header of the calltable envelope:

hex formatted bytesInterpretation
0x01000000le-encoded number of entries in the calltable header for TransactionScheduling
0x0000le encoded index of the first calltable entry: 0 means variant discriminator of the union type (since TransactionScheduling is a union type)
0x00000000le encoded offset of the first calltable entry - means that the variant discriminator starts at byte 0 of the envelope payload.

Interpretation of the bytes representing payload of the calltable envelope:

hex formatted bytesInterpretation
0x010000001 (4-bytes unsigned LE) - number of bytes in the paylod
0x000 value of the discriminator - deserializes to Standard variant. This variant has no fields

deserializing Approvals

Previously we established that the approvals raw bytes are:

0x0100000001d9bf2148748a85c89da5aad8ee0b0fc2d105fd39d41a4c796536
354f0ae2900c013104d575cdd9a865a54eae377586dd2ea9912fde9511d3471f
63a73c1162b7f6c7a290611a527fc14247e4eb86c308c27f45d2ece4abc50629
1c600c54ee720b

We can deserialize the above as collection of approvals

Helper script

The binary payload used in this example can be reproduced using the following script written in rust and facilitating the reference implementation casper-types library.

    use casper_types::{
Approval, Digest, InitiatorAddr, PricingMode, PublicKey, RuntimeArgs, SecretKey, TimeDiff,
Timestamp, Transaction, TransactionArgs, TransactionEntryPoint, TransactionHash,
TransactionScheduling, TransactionTarget, TransactionV1, TransactionV1Hash,
TransactionV1Payload, bytesrepr::ToBytes,
};
use std::collections::{BTreeMap, BTreeSet};

let signer_secret_key =
SecretKey::ed25519_from_bytes([15u8; SecretKey::ED25519_LENGTH]).unwrap();
let signer_public_key = PublicKey::from(&signer_secret_key);
let timestamp = Timestamp::from(123);
let ttl = TimeDiff::from_seconds(3600);
let pricing_mode = PricingMode::PaymentLimited {
payment_amount: 567_891_u64,
gas_price_tolerance: 1,
standard_payment: true,
};
let initiator_addr = InitiatorAddr::PublicKey(signer_public_key);
let mut fields = BTreeMap::new();

let arg = TransactionArgs::Named(RuntimeArgs::new());
let arg_bytes = arg
.to_bytes()
.expect("should be able to serialize transaction args to bytes");
fields.insert(0_u16, arg_bytes.into());

let transaction_target = TransactionTarget::Native;
let target_bytes = transaction_target
.to_bytes()
.expect("should be able to serialize transaction_target to bytes");
fields.insert(1_u16, target_bytes.into());

let entry_point = TransactionEntryPoint::Delegate;
let entry_point_bytes = entry_point
.to_bytes()
.expect("should be able to serialize entry_point to bytes");
fields.insert(2_u16, entry_point_bytes.into());

let scheduling = TransactionScheduling::Standard;
let scheduling_bytes = scheduling
.to_bytes()
.expect("should be able to serialize scheduling to bytes");
fields.insert(3_u16, scheduling_bytes.into());

let payload = TransactionV1Payload::new(
"my-chain".to_owned(),
timestamp,
ttl,
pricing_mode,
initiator_addr,
fields,
);
let payload_bytes = payload
.to_bytes()
.expect("It should be possible to turn payload into bytes");
let hash = Digest::hash(payload_bytes);
let approval = Approval::create(
&TransactionHash::V1(TransactionV1Hash::from(hash)),
&signer_secret_key,
);
let mut approvals = BTreeSet::new();
approvals.insert(approval);

let transaction_v1 = TransactionV1::new(hash.into(), payload, approvals);
let transaction = Transaction::V1(transaction_v1);
println!(
"{}",
hex::encode(
transaction
.to_bytes()
.expect("Expected transaction to correctly serialize")
)
);