Specification for the Rewards Merkle Tree

Rocket Pool’s Redstone update introduced a new rewards system based on a Merkle Tree distribution system. For each rewards interval, this tree will capture the RPL rewards (both for node operation and Oracle DAO membership) and the ETH rewards (from the Smoothing Pool, if relevant) for all of the nodes in the Rocket Pool network.

In the Houston update, following RPIP-53 the tree was modified slightly to account for the new RPL withdrawal address feature.

The following is a specification that formally describes the tree’s construction and behavior for rewards specification version 10 and higher. Users should be able to use this specification to independently verify the tree that is produced by the Oracle DAO.

Overall Structure

Tree Size and Leaf Nodes

The Rewards Merkle Tree is a complete binary tree, where each node has exactly two children (with the exception of leaf nodes, which have zero). Therefore, the total size of the tree is always a power of 2, minus 1.

Fundamental to each Merkle Tree is the data that is used to produce the leaf nodes of the tree. Leaf nodes correspond to unique Execution layer addresses, called claimers. They can represent any address; they do not specifically require the address to be a registered Rocket Pool node address. For each unique address in the tree, there will be exactly one corresponding leaf node. There will not be multiple leaf nodes corresponding to the same address.

The value of the leaf node will be the Keccak256 hash of the claimer’s metadata (described further below).

If the total number of tree nodes is not a power of 2, all of the remaining leaf nodes will have the null hash (0x0000…0000) instead of the hashed node metadata to pad the total count to a power of 2. For example, if there were 9 claimers, they would each constitute a leaf node of the tree and there would be 7 supplemental “null” leaf nodes to bring the total leaf count to 16 (the next highest power of 2).

Sorting Leaf Nodes

Prior to tree generation, claimers must be determined and their metadata must be hashed to produce the values of the corresponding leaf nodes. This process is described in the Leaf Node Values section below.

The hashes should be recorded in big-endian format. This collection must then be sorted lexicographically by these hash values, so the leaf node with the lowest value becomes the leftmost (first) leaf node. The leaf node with the second-lowest value must be the second leaf node, and so on.

Missing leaf nodes filled in with null hash values (0x0000...0000) will be used to pad the tree at the end of this sorted list, not before it. This will cause those leaf nodes to be out of order lexicographically (as 0x0000...0000 would normally come before anything else).

For example, if there were 9 claimers that generated the following leaf node values from their hashed metadata:

0xb50d..., 0x213f..., 0xb000..., 0x2c23..., 0x2bc1..., 0x5efe..., 0xf4d7..., 0x91b2..., 0xe5ed...

These leaf nodes would be sorted into the following order prior to generating the tree (including the null-hash padding at the end):

0x213f..., 0x2bc1..., 0x2c23..., 0x5efe..., 0x91b2..., 0xb000..., 0xb50d..., 0xe5ed..., 0xf4d7..., 0x0000...., 0x0000...., 0x0000...., 0x0000...., 0x0000...., 0x0000...., 0x0000....

Branch Nodes

To find the value of the branch node, use the following procedure:

  1. Get the value of the left child node (which is a Keccak256 hash). Call this left.
  2. Get the value of the right child node (which is a Keccak256 hash). Call this right.
  3. Sort these values lexicographically; that is, if right is lower than left, switch left and right.
    • NOTE: don’t reorder the child nodes in the tree itself, just switch the values of the left and right local variables.
  4. Concatenate the two hashes (which are both 32 bytes) into a 64-byte array where left represents the first 32 bytes and right represents the last 32 bytes.
  5. Calculate the Keccak256 hash of this 64-byte array. This hash is the value of this branch node.

Consider the following example:

left = 0xfdbbe597834a953e4c4e50fd8ae8859fd4ae6bf808eb72139ee5a4c224e695f9
right = 0xad1d32ebc492ff5ad2ab148049de34db5b6d45b9d467823470dffb4c18a4a337

In this case, the concatenated byte array would be 0xad1d32ebc492ff5ad2ab148049de34db5b6d45b9d467823470dffb4c18a4a337fdbbe597834a953e4c4e50fd8ae8859fd4ae6bf808eb72139ee5a4c224e695f9 (note the reordering due to the sorting here).

The Keccak256 hash of this would be 0xb079b0168e5beba73f17c52b76a614539b242d8efcf6bb99e0dd66a2e251e9e7, which becomes the value of this branch node.

Leaf Node Values

Start by determining the list of claimers following the rewards calculation specification.

Each leaf node of the tree will be the the Keccak256 hash of a single claimer’s metadata. The structure for this metadata is as follows:

metadata = address[20] ++ network[32] ++ totalRPL[32] ++ totalETH[32]

where address[20] refers to a byte array of size 20 bytes, and the ++ operator refers to byte array concatenation.

Each of these values is defined as follows:

  • address is the standard 20-byte Ethereum address of the claimer.
  • network is a 256-bit (32-byte) unsigned integer representing which Rocket Pool network the rewards will be available on.
    • 0 represents Mainnet.
    • More values will be added as we integrate support for various L2s to the rewards system. Currently, 0 is the only legal value for this.
  • totalRPL is a 256-bit (32-byte) unsigned integer representing how much RPL the claimer is eligible to claim for this rewards period.
    • This value is the sum of the RPL rewards provided for staking minipools and the RPL rewards provided for ODAO membership.
    • The RPL value is expected in wei, so rewards of 2 RPL would be represented as 2000000000000000000 (in decimal).
  • totalETH is a 256-bit (32-byte) unsigned integer representing how much ETH is eligible to claim from the Smoothing Pool for this rewards period.
    • The ETH value is expected in wei, so rewards of 2 ETH would be represented as 2000000000000000000 (in decimal).

In total, the metadata will be an array of 116 bytes.

For example, say there were a claimer with the following values in its metadata:

  • Address: 0x8B0EF9f1932A2e44c3D27bE4C70C3BC07A6A27B3
  • Network: 0
  • Total RPL: 1.5 RPL (1500000000000000000 in wei which is 14d1120d7b160000 in hexadecimal)
  • Total ETH: 2.5 ETH (2500000000000000000 in wei which is 22b1c8c1227a0000 in hexadecimal)

It would have the following metadata byte array (in hexadecimal):

0x8b0ef9f1932a2e44c3d27be4c70c3bc07a6a27b3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014d1120d7b16000000000000000000000000000000000000000000000000000022b1c8c1227a0000

Taking the Keccak256 hash of this value will produce the value of the corresponding leaf node in the tree for this Rocket Pool node.

Complete Example

To do a complete example of the tree structure, consider the following 9 claimers where the RPL and ETH values are already provided as decimal strings in wei:

"0x14cb2253a2F9898EFA43b9ca15bCFDE401CCFbe7": {
    "network": 0,
    "amountRPL": "2000000000000000000",
    "amountETH": "0"
},
"0x18A58E43c37DdC9ccCf3AC642c6f430ad663E400": {
    "network": 0,
    "amountRPL": "1333000000000000000",
    "amountETH": "300000000000000000"
},
"0x24fBeD7Ecd625D3f0FD19a6c9113DEd436172294": {
    "network": 0,
    "amountRPL": "2000000000000000000",
    "amountETH": "1000000000000000000"
},
"0x6f10Fd508321D27D8F19CBCC2F2f3d5527B637eC": {
    "network": 0,
    "amountRPL": "100000000000000000",
    "amountETH": "300000000000000000"
},
"0x7e9eA400443957BE3918acfbd1f57cF6D3f5126A": {
    "network": 0,
    "amountRPL": "1000000000000000000",
    "amountETH": "0"
},
"0x822Eaeebb9e106C8CB263bDa6455430fEC652653": {
    "network": 0,
    "amountRPL": "0",
    "amountETH": "2000000000000000000"
},
"0x8B0EF9f1932A2e44c3D27bE4C70C3BC07A6A27B3": {
    "network": 0,
    "amountRPL": "1500000000000000000",
    "amountETH": "2500000000000000000"
},
"0xD57D9c08926E28fC9F1f3dd65Db02e6a7958380c": {
    "network": 0,
    "amountRPL": "100000000000000000",
    "amountETH": "300000000000000000"
},
"0xe6ED92D26573c67AF5eca7fB2a49A807FB8f88dB": {
    "network": 0,
    "amountRPL": "1333000000000000000",
    "amountETH": "300000000000000000"
}

These leaf nodes would generate the following tree according to the specification described here: