Skip to content

Instantly share code, notes, and snippets.

@sisou
Created July 26, 2024 13:43
Show Gist options
  • Save sisou/33ece69190cf38f884b1781ad9d5a106 to your computer and use it in GitHub Desktop.
Save sisou/33ece69190cf38f884b1781ad9d5a106 to your computer and use it in GitHub Desktop.
Albatross Transaction Serialization (Basic)

Albatross Transaction Serialization (Basic)

Transaction Creation

We'll be creating a basic transaction signed with and sent from the following private key (all zeroes):

0000000000000000000000000000000000000000000000000000000000000000

This private key hashes to this public key:

3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29

and this address:

hex: 689dae2f77b048dcc08e14d73104ea14222b5be1
userfriendly: NQ17 D2ES UBTP N14D RG4E 2KBK 217A 2GH2 NNY1

So that will be the sender account. For the recipient, we'll chose this address for easy recognition:

hex: 1111111111111111111111111111111111111111
userfriendly: NQ34 248H 248H 248H 248H 248H 248H 248H 248H

The full transaction details then look like this:

Sender NQ17 D2ES UBTP N14D RG4E 2KBK 217A 2GH2 NNY1
Recipient NQ34 248H 248H 248H 248H 248H 248H 248H 248H
Value 1000 NIM
Fee 138 luna (= 0.00138 NIM)
Validity Start Height 100000
Network ID 5 (Albatross Testnet)

We can create this transaction with the Nimiq Albatross JS library like so:

const tx = Nimiq.TransactionBuilder.newBasic(
    Nimiq.Address.fromString("NQ17 D2ES UBTP N14D RG4E 2KBK 217A 2GH2 NNY1"),
    Nimiq.Address.fromString("NQ34 248H 248H 248H 248H 248H 248H 248H 248H"),
    BigInt(1000 * 1e5), // value
    BigInt(138),        // fee
    100000,             // validity start height
    5,                  // network ID
);

Serialization for Signing

For signing with our private key, the transaction is serialized in a backward-compatible way with Nimiq's Proof-of-Work network with this method:

tx.serializeContent()

This gives us the following byte array:

[0, 0, 104, 157, 174, 47, 119, 176, 72, 220, 192, 142, 20, 215, 49, 4, 234, 20,
34, 43, 91, 225, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 0, 0, 0, 0, 0, 5, 245, 225, 0, 0, 0, 0, 0, 0, 0, 0, 138, 0,
1, 134, 160, 5, 0, 0]

which translates to this hex string by wrapping it with Nimiq.BufferUtils.toHex() (split here into individual bytes for representation):

00 00 68 9d ae 2f 77 b0 48 dc c0 8e 14 d7 31 04 ea 14 22 2b 5b e1 00 11 11 11
11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 00 00 00 00 00 05 f5 e1 00
00 00 00 00 00 00 00 8a 00 01 86 a0 05 00 00

Let's disect its parts.

Recipient Data Length

00 00

We start with the recipient data length. This is always encoded as a uint16, so it takes two bytes. There is no recipient data in our basic transaction, so the length is 0 and no recipient data is following.

Sender Address

689dae2f77b048dcc08e14d73104ea14222b5be1

These 20 bytes represent the sender address. If you compare it to the address generated from our all-zeroes private key above, you'll notice these are the same bytes.

Sender Type

00

Following the sender address is the sender type. Type 0 stands for "basic" account, which means regular, private-key controlled accounts without special functionality. Vesting accounts are type 1, HTLCs are type 2 and the staking contract is type 3.

Recipient Address

1111111111111111111111111111111111111111

Next up is the recipient address, again 20 bytes. Remember we chose this address above to be easily recognizable, and it is!

Recipient Type

00

Just like the sender address was followed by the sender type, this is the recipient type following the recipient address. Again, type 0 for "basic" address. If we were to create a contract or interact with the staking contract, we'd have to change this byte accordingly to represent the recipient type.

Value

0000000005f5e100

This is the transaction's value, encoded as a big-endian u64, which takes 8 bytes. If you put this hex string into Javascript's parseInt("0000000005f5e100", 16), you'll get 100000000, which is 1000 NIM in lunas (multiplied by 1e5).

Fee

000000000000008a

Following the value is the transaction's fee, also encoded as big-endian u64. Parsing this hex string into a decimal number yields 138 (in lunas), which is exactly what we set as the fee when we created the transaction.

Validity Start Height

000186a0

This is the validity start height of the transaction, encoded as a big-endian u32, which takes 4 bytes. Parsing it to decimal gives 100000 which we put into the transaction creation above.

Network ID

05

The next byte is the network ID. Since we don't expect a lot of different networks, we use a single-byte u8 to encode it.

Flags

00

Transactions can have bitflags, encoded in this byte. 0 means there are no flags set. 0b00000001 = 1 is for contract creation transactions, 0b00000010 = 2 is for signalling transactions (interactions with the staking contract that don't transfer any value).

Sender Data Length

00

The last byte is the length of optional sender data. Sender data is only used when withdrawing NIM from the staking contract (to encode if the NIM are withdrawn from a validator or a staker). Thus for our transaction, the sender data is empty, which is represented here with a length of 0. While the recipient data length at the beginning of the serialization is always encoded as a u16 and thus always takes 2 bytes, this length here is instead a varint, which can have different byte lengths, depending on which number it encodes. Since sender data in Albatross has a maximum length of 1 byte, this sender data length byte will only ever encode the numbers 0 or 1, so it will always be 1 byte, too.

Since the sender data length is 0, no sender data is following and the serialization for signing is done.

The signature of the private key is now created over these bytes. In Nimiq's case we usually work with Ed25519 keys, so this will be an Ed25519 signature.

Serialization for Sending

Once we signed the transaction, the signature proof gets attached to it and we can now serialize the whole transaction, ready for broadcasting, with this method:

tx.serialize()

This gives us the following byte array:

[0, 0, 59, 106, 39, 188, 206, 182, 164, 45, 98, 163, 168, 208, 42, 111, 13,
115, 101, 50, 21, 119, 29, 226, 67, 166, 58, 192, 72, 161, 139, 89, 218, 41,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
0, 0, 0, 0, 5, 245, 225, 0, 0, 0, 0, 0, 0, 0, 0, 138, 0, 1, 134, 160, 5, 233,
125, 20, 229, 171, 139, 158, 155, 113, 247, 210, 149, 36, 87, 129, 15, 245,
200, 199, 98, 171, 146, 221, 237, 133, 46, 185, 21, 237, 56, 225, 240, 193, 51,
42, 188, 237, 42, 109, 236, 102, 204, 76, 191, 208, 37, 222, 150, 9, 113, 37,
130, 135, 47, 148, 234, 188, 103, 100, 75, 77, 79, 54, 14]

To better understand and compare to the other serialization, we'll also convert this to hex (either again with Nimiq.BufferUtils.toHex() or simply with tx.toHex()):

00 00 3b 6a 27 bc ce b6 a4 2d 62 a3 a8 d0 2a 6f 0d 73 65 32 15 77 1d e2 43 a6
3a c0 48 a1 8b 59 da 29 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11
11 11 00 00 00 00 05 f5 e1 00 00 00 00 00 00 00 00 8a 00 01 86 a0 05 e9 7d 14
e5 ab 8b 9e 9b 71 f7 d2 95 24 57 81 0f f5 c8 c7 62 ab 92 dd ed 85 2e b9 15 ed
38 e1 f0 c1 33 2a bc ed 2a 6d ec 66 cc 4c bf d0 25 de 96 09 71 25 82 87 2f 94
ea bc 67 64 4b 4d 4f 36 0e

Let's go through these bytes.

Format

00

Transactions in Nimiq have one of two formats: "basic" and "extended". Basic transactions are between to "basic" accounts and do not contain any recipient nor sender data. "Basic" transactions can thus be serialized more efficiently than "extended" transactions, which must cover every other use case.

The first byte represents this format. 0 for "basic", 1 for "extended". Here we have a "basic" transaction.

Signature Proof Type and Flags

00

Transactions in Nimiq can have one of two signature types: "EdDSA" and "ECDSA". Traditionally in its Proof-of-Work network, Nimiq only supported EdDSA signatures over Ed25519. With the introduction of Webauthn (Passkey) signatures in Albatross, Nimiq now supports ECDSA signatures (like Bitcoin's), too. EdDSA signatures are type 0 and ECDSA signatures are type 1. Additionally, this first byte carries a flag to specify if the signature has Webauthn fields. The first (upper) 4 bits of this byte are for the flags, the last (lower) 4 bits are for the signature type.

Here we have a regular Ed25519 signature without Webauthn fields, so no flags and the type is 0. Thus the whole byte is 0.

Sender Public Key

3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29

This is the 32-byte public key of the sender of the transaction (compare it with the public key derived from our all-zeroes private key at the top of the article), which serves a double purpose: it declares both the sender address (which can be derived from this public key), and the signer's public key at the same time.

"Extended" transactions contain both the sender address and the signer public key, as those can be unconnected in some use-cases (e.g. when withdrawing from a contract or interacting with the staking contract). For "basic" transactions this cannot happen, so this is one example of how a "basic" transaction is serialized more efficiently.

Recipient address

1111111111111111111111111111111111111111

The next 20 bytes are the recipient address. Because the format of the transaction has already been established as "basic", we do not need to encode any sender or recipient account types, as those must be "basic" in "basic" transactions.

Value, Fee, Validity Start Height, Network ID

0000000005f5e100 # value
000000000000008a # fee
000186a0         # validity start height
05               # network ID

The transaction's value and fee, validity start height and network ID are all encoded like in the signing serialization explained above.

Signature

e97d14e5ab8b9e9b71f7d2952457810ff5c8c762ab92dded852eb915ed38e1f0c1332abced2a6dec66cc4cbfd025de9609712582872f94eabc67644b4d4f360e

At the end of the serialization comes the signature, which is 64 bytes long. Since the proof-type-and-flags byte at the beginning of the serialization specified that this is not a Webauthn signature, there are no more bytes following and this is the end of the serialization.

With this, the transaction can be broadcast to the network via a Nimiq Albatross node (for example with the RPC's sendRawTransaction method).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment