Transactions and Blocks

How transactions and blocks work in Blockchain Development Kit (BDK).

This subchapter explains the structure of blocks, transactions, and how both are properly parsed within BDK and AppLayer.

How transactions are parsed

No matter the type, there are two ways a transaction can be parsed from a bytes string:

  • Directly from RLP

    • Requires deriving the sender (from) address and a validity check using secp256k1

    • Not included in a block, which means it's a new transaction coming from the network

    • Equivalent to Ethereum's rawTransaction

  • Directly from the database

    • Considered trustworthy since it already went through the process above

    • Is included in a block, therefore it's part of the blockchain

Transaction structure

Depending on the type, a transaction will contain the following data fields once it's properly parsed:

  • (TxBlock) to - receiver address (the one that received the funds)

  • (Both) from - sender address (the one that sent the funds)

  • (Both) data - arbitrary data field (used in contracts)

  • (Both) chainId - unique blockchain ID where the transaction was made

  • (TxValidator) nHeight - height of the block where the transaction was included

  • (TxBlock) nonce - number of the transaction made from the sender address - always starts at 0, so a nonce of 4 means this is the fifth transaction from the address

  • (TxBlock) value - transaction value in its lowest unit

    • e.g. "satoshi" for BTC, "Wei" for ETH, etc. - "100000000" satoshis would be 1 BTC, "5000000000" Wei would be 0.000000005 ETH (or 5 gwei), so on and so forth

    • Since we're using an Ethereum-based format, we commonly refer to its terminology, so "value" is in "Wei"

  • (TxBlock) gasLimit - maximum limit of gas units that the transaction will use, in Wei (e.g. "21000")

    • If the transaction uses more than this limit, it will automatically fail - the original transaction value won't be spent, but what was already spent as gas is lost

  • (TxBlock) maxPriorityFeePerGas - value paid as an incentive for miners to include the transaction in the block, as per EIP-1559

    • This is implemented but actively ignored since we don't have "miners", therefore only maxFeePerGas is counted

  • (TxBlock) maxFeePerGas - value paid by every unit of gas, in Gwei (e.g. "15" = 15000000000 Wei)

    • The total transaction fee is calculated as (gasLimit * maxFeePerGas) - e.g. 21000 * 15000000000 = 0.000315 ETH

  • (Both) ECDSA signature (Elliptic Curve Digital Signature Algorithm) for validating transaction integrity, split in three:

    • r - first half of the ECDSA signature (32 hex bytes)

    • s - second half of the ECDSA signature (32 hex bytes)

    • v - recovery ID (1 hex byte)

Block structure

Depending on the type, a block will contain the following conceptual structure:

  • (FinalizedBlock only) Validator signature (the one responsible for creating and signing the block)

  • (FinalizedBlock only) Validator public key (for verifying the signature)

  • The block's header, which is made of:

    • (Both) Previous block hash (a hash of the previous block's header, signed by the Validator)

    • (Both) "Randomness" (a random seed generated by RandomGen during the previous block's creation)

    • (FinalizedBlock only) Validator Tx Merkle Tree (to verify the integrity of Validator transactions)

    • (FinalizedBlock only) Block Tx Merkle Tree (to verify the integrity of Block transactions)

    • (Both) UNIX timestamp of the block, in microseconds

    • (Both) Block height (commonly known as nHeight)

  • (Both) List of block transactions

  • (Both) List of Validator transactions

  • (Both) A cached hash of the block's header

In practice, a block is simply a serialized bytes string, transmitted through the network and stored in the blockchain, so it's up to the code logic to properly parse it. Another way to see the block in a more "raw" format would be ("bytes" are in hex format, e.g. 0xFF = 1 byte):

Outside header:
  65 bytes - Validator signature
  65 bytes - Validator public key
Header (144 bytes):
  32 bytes - Previous block hash
  32 bytes - "Randomness"
  32 bytes - Validator Tx Merkle Root
  32 bytes - Block Tx Merkle Root
  8 bytes - Timestamp (in microseconds)
  8 bytes - Block height
Content:
  8 bytes - Offset of Validator transactions array
  [
    4 bytes - Block transaction size
    N bytes - Block transaction
    ...
  ]
  [
    4 bytes - Validator transaction size
    N bytes - Validator transaction
    ...
  ]

Blocks transmitted through the network must have the Validator signature, which is part of the block but is outside its structure. This is intentional, so the block header can be hashed and signed on its own without hashing the signature along with it.

Raw block analysis

A "raw" finalized block would look something like this:

5c37d504e9415c3b75afaa3ad24484382274bba31f10dcd268e554785d5b807500000181810eb6507a8b54dfbfe9f21d00000001000000aff8ad81be850c92a69c0082e18c94d586e7f844cea2f87f50152665bcbc2c279d8d7080b844a9059cbb00000000000000000000000026548521f99d0709f615aa0f766a7df60f99250b00000000000000000000000000000000000000000000002086ac351052600000830150f7a07e16328b7f3823abeb13d0cab11cdffaf967c9b2eaf3757c42606d6f2ecd8ce6a040684c94b289cdda2282...

Where:

  • Validator signature = 5c37d504e9415c3b75afaa3ad24484382274bba31f10dcd268e554785d5b807500000181810eb6507a8b54dfbfe9f21d00000001000000aff8ad81be850c92a69c

  • Previous block hash = 0082e18c94d586e7f844cea2f87f50152665bcbc2c279d8d7080b844a9059cbb

  • "Randomness" = 00000000000000000000000026548521f99d0709f615aa0f766a7df60f99250b

  • Validator Merkle Tree = 00000000000000000000000000000000000000000000002086ac351052600000

  • Block Merkle Tree = 830150f7a07e16328b7f3823abeb13d0cab11cdffaf967c9b2eaf3757c42606d

  • Timestamp = 6f2ecd8ce6a04068

  • Block height = 4c94b289cdda2282

  • Rest of the content = ...

Last updated