celo-org / celo-blockchain

Official repository for the golang Celo Blockchain
https://celo.org
GNU Lesser General Public License v3.0
560 stars 198 forks source link

Unable to re-create receipts root only for block 22982400 #2210

Closed sirpy closed 8 months ago

sirpy commented 10 months ago

Expected Behavior

merkle-patricia-tree root based on receipts should match the block reported receipts root hash

Actual Behavior

no match

Steps to reproduce the behavior

I'm using merkle-patricia-tree package to generate the tries and proofs. This has been working just fine except for this block 22982400 for later blocks it also works, when I build the tree from the receipts the final root doesn't match the one reported in the block header

Chain/Network: Mainnet

code:

import { BaseTrie as Tree } from 'merkle-patricia-tree';
import { Receipt } from 'eth-object';
import { encode } from 'eth-util-lite';

const targetReceipt = await provider.send('eth_getTransactionReceipt', [txHash]);
const rpcBlock = await provider.send('eth_getBlockByHash', [targetReceipt.blockHash, false]);
const receipts = (
    await Promise.all(
      rpcBlock.transactions
        .map(async (siblingTxHash) => {
          return provider.send('eth_getTransactionReceipt', [siblingTxHash]);
        })
    )
  ).filter((_) => _);

  const tree = new Tree();

  await Promise.all(
    receipts.map((siblingReceipt, index) => {
      const siblingPath = encode(index);
      let serializedReceipt = Receipt.fromRpc(siblingReceipt).serialize();
      if (siblingReceipt.type && siblingReceipt.type != '0x0') {
        serializedReceipt = Buffer.concat([Buffer.from([siblingReceipt.type]), serializedReceipt]);
      }
      return tree.put(siblingPath, serializedReceipt);
    }),
  );

  const receiptsRoot = '0x' + tree.root.toString('hex');
  if (receiptsRoot !== rpcBlock.receiptsRoot) {
    console.error(receipts, {
      receiptsRoot,
      blockReceiptsRoot: blockHeader.block.receiptsRoot,
    });
    throw new Error('receiptsRoot mismatch');
  }
karlb commented 9 months ago

Thanks for the bug report! I can reproduce your observation that the receipts root for block 22982400 does not match the merkle root calculated from the receipts that are returned via RPC.

Block 22982400 is an epoch block and it looks like this mismatch happens for all epoch blocks and all other blocks are fine. Epoch blocks have special epoch transactions to distribute the epoch rewards rewards (e.g. rewards for block 22982400). These won't show up as normal transactions in the RPC. I assume that the epoch transactions are the reason for the mismatch, although I have not yet looked up how receipts for them are handled.

karlb commented 8 months ago

When events are emitted by the block processing outside of user transaction (e.g. epoch rewards), then one "block receipt" is added to the block. This receipt can't be fetched with eth_getTransactionReceipt but with the Celo-specific eth_getBlockReceipt.

Combining these receipts makes the receiptsRoot match again:

import { ethers } from "ethers";
import { BaseTrie as Tree } from 'merkle-patricia-tree';
import { Receipt } from 'eth-object';
import { encode } from 'eth-util-lite';

const provider = new ethers.JsonRpcProvider('https://forno.celo.org');

const block = '0x' + (22982400).toString(16);
const rpcBlock = await provider.send('eth_getBlockByNumber', [block, false]);
const transactionReceipts = (
  await Promise.all(
    rpcBlock.transactions
    .map(async (siblingTxHash) => {
      return provider.send('eth_getTransactionReceipt', [siblingTxHash]);
    })
  )
)
const blockReceipt = (
  await Promise.all(
    [rpcBlock.hash]
    .map(async (siblingTxHash) => {
      return provider.send('eth_getBlockReceipt', [siblingTxHash]);
    })
  )
)
const receipts = transactionReceipts.concat(blockReceipt);

const tree = new Tree();

await Promise.all(
  receipts.map((siblingReceipt, index) => {
    const siblingPath = encode(index);
    let serializedReceipt = Receipt.fromRpc(siblingReceipt).serialize();
    if (siblingReceipt.type && siblingReceipt.type != '0x0') {
      serializedReceipt = Buffer.concat([Buffer.from([siblingReceipt.type]), serializedReceipt]);
    }
    return tree.put(siblingPath, serializedReceipt);
  }),
);

const receiptsRoot = '0x' + tree.root.toString('hex');
if (receiptsRoot !== rpcBlock.receiptsRoot) {
  console.error(receipts, {
    receiptsRoot,
    blockReceiptsRoot: rpcBlock.receiptsRoot,
  });
  throw new Error('receiptsRoot mismatch');
}

I admit that this situation is currently under-documented.