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
);
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.
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.
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.
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.
1111111111111111111111111111111111111111
Next up is the recipient address, again 20 bytes. Remember we chose this address above to be easily recognizable, and it is!
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.
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).
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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).