|
| 1 | +// Copyright 2025-2026, Offchain Labs, Inc. |
| 2 | +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE.md |
| 3 | + |
| 4 | +package main |
| 5 | + |
| 6 | +import ( |
| 7 | + "bytes" |
| 8 | + "context" |
| 9 | + "errors" |
| 10 | + "fmt" |
| 11 | + |
| 12 | + "github.com/ethereum/go-ethereum/common" |
| 13 | + "github.com/ethereum/go-ethereum/core/types" |
| 14 | + "github.com/ethereum/go-ethereum/rlp" |
| 15 | + |
| 16 | + "github.com/offchainlabs/nitro/arbutil" |
| 17 | +) |
| 18 | + |
| 19 | +type receiptFetcherForBlock struct { |
| 20 | + header *types.Header |
| 21 | + preimageResolver preimageResolver |
| 22 | +} |
| 23 | + |
| 24 | +// ReceiptForTransactionIndex fetches a receipt for a specific transaction index by walking |
| 25 | +// the receipt trie of the block header. It uses the preimage resolver to fetch the preimages |
| 26 | +// of the trie nodes as needed. |
| 27 | +func (rf *receiptFetcherForBlock) ReceiptForTransactionIndex( |
| 28 | + ctx context.Context, |
| 29 | + txIndex uint, |
| 30 | +) (*types.Receipt, error) { |
| 31 | + return fetchReceiptFromBlock(rf.header.ReceiptHash, txIndex, rf.preimageResolver) |
| 32 | +} |
| 33 | + |
| 34 | +// Fetches a specific receipt index from a block's receipt trie by navigating its |
| 35 | +// Merkle Patricia Trie structure. It uses the preimage resolver to fetch preimages |
| 36 | +// of trie nodes as needed, and determines how to navigate depending on the structure of the trie nodes. |
| 37 | +func fetchReceiptFromBlock( |
| 38 | + receiptsRoot common.Hash, |
| 39 | + receiptIndex uint, |
| 40 | + preimageResolver preimageResolver, |
| 41 | +) (*types.Receipt, error) { |
| 42 | + currentNodeHash := receiptsRoot |
| 43 | + currentPath := []byte{} // Track nibbles consumed so far. |
| 44 | + receiptKey, err := rlp.EncodeToBytes(receiptIndex) |
| 45 | + if err != nil { |
| 46 | + return nil, err |
| 47 | + } |
| 48 | + targetNibbles := keyToNibbles(receiptKey) |
| 49 | + for { |
| 50 | + nodeData, err := preimageResolver.ResolveTypedPreimage(arbutil.Keccak256PreimageType, currentNodeHash) |
| 51 | + if err != nil { |
| 52 | + return nil, err |
| 53 | + } |
| 54 | + var node []any |
| 55 | + if err = rlp.DecodeBytes(nodeData, &node); err != nil { |
| 56 | + return nil, fmt.Errorf("failed to decode RLP node: %w", err) |
| 57 | + } |
| 58 | + switch len(node) { |
| 59 | + case 17: |
| 60 | + // We hit a branch node, which has 16 children and a value. |
| 61 | + if len(currentPath) == len(targetNibbles) { |
| 62 | + // A branch node's 17th item could be the value, so we check if it contains the receipt. |
| 63 | + if valueBytes, ok := node[16].([]byte); ok && len(valueBytes) > 0 { |
| 64 | + // This branch node has the actual value as the last item, so we decode the receipt |
| 65 | + return decodeReceipt(valueBytes) |
| 66 | + } |
| 67 | + return nil, fmt.Errorf("no receipt found at target key") |
| 68 | + } |
| 69 | + // Get the next nibble to follow. |
| 70 | + targetNibble := targetNibbles[len(currentPath)] |
| 71 | + childData, ok := node[targetNibble].([]byte) |
| 72 | + if !ok || len(childData) == 0 { |
| 73 | + return nil, fmt.Errorf("no child at nibble %d", targetNibble) |
| 74 | + } |
| 75 | + // Move to the child node, which is the next hash we have to navigate. |
| 76 | + currentNodeHash = common.BytesToHash(childData) |
| 77 | + currentPath = append(currentPath, targetNibble) |
| 78 | + case 2: |
| 79 | + keyPath, ok := node[0].([]byte) |
| 80 | + if !ok { |
| 81 | + return nil, fmt.Errorf("invalid key path in node") |
| 82 | + } |
| 83 | + key := extractKeyNibbles(keyPath) |
| 84 | + expectedPath := make([]byte, 0) |
| 85 | + expectedPath = append(expectedPath, currentPath...) |
| 86 | + expectedPath = append(expectedPath, key...) |
| 87 | + |
| 88 | + // Check if it is a leaf or extension node. |
| 89 | + leaf, err := isLeaf(keyPath) |
| 90 | + if err != nil { |
| 91 | + return nil, err |
| 92 | + } |
| 93 | + if leaf { |
| 94 | + // Check that the keyPath matches the target nibbles, |
| 95 | + // otherwise, the receipt does not exist in the trie. |
| 96 | + if !bytes.Equal(expectedPath, targetNibbles) { |
| 97 | + return nil, fmt.Errorf("leaf key does not match target nibbles") |
| 98 | + } |
| 99 | + rawData, ok := node[1].([]byte) |
| 100 | + if !ok { |
| 101 | + return nil, fmt.Errorf("invalid receipt data in leaf node") |
| 102 | + } |
| 103 | + return decodeReceipt(rawData) |
| 104 | + } |
| 105 | + // If the node is not a leaf node, it is an extension node. |
| 106 | + // Check if our target key matches this extension path. |
| 107 | + if len(expectedPath) > len(targetNibbles) || !bytes.Equal(expectedPath, targetNibbles[:len(expectedPath)]) { |
| 108 | + return nil, fmt.Errorf("extension path mismatch") |
| 109 | + } |
| 110 | + nextNodeBytes, ok := node[1].([]byte) |
| 111 | + if !ok { |
| 112 | + return nil, fmt.Errorf("invalid next node in extension") |
| 113 | + } |
| 114 | + // We navigate to the next node in the trie. |
| 115 | + currentNodeHash = common.BytesToHash(nextNodeBytes) |
| 116 | + currentPath = expectedPath |
| 117 | + default: |
| 118 | + return nil, fmt.Errorf("invalid node structure: unexpected length %d", len(node)) |
| 119 | + } |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// Converts a byte slice key into a slice of nibbles (4-bit values). |
| 124 | +// Keys are encoded in big endian format, which is required by Ethereum MPTs. |
| 125 | +func keyToNibbles(key []byte) []byte { |
| 126 | + nibbles := make([]byte, len(key)*2) |
| 127 | + for i, b := range key { |
| 128 | + nibbles[i*2] = b >> 4 |
| 129 | + nibbles[i*2+1] = b & 0x0f |
| 130 | + } |
| 131 | + return nibbles |
| 132 | +} |
| 133 | + |
| 134 | +// Extracts the key nibbles from a key path, handling odd/even length cases. |
| 135 | +func extractKeyNibbles(keyPath []byte) []byte { |
| 136 | + if len(keyPath) == 0 { |
| 137 | + return nil |
| 138 | + } |
| 139 | + nibbles := keyToNibbles(keyPath) |
| 140 | + if nibbles[0]&1 != 0 { |
| 141 | + return nibbles[1:] |
| 142 | + } |
| 143 | + return nibbles[2:] |
| 144 | +} |
| 145 | + |
| 146 | +func isLeaf(keyPath []byte) (bool, error) { |
| 147 | + firstByte := keyPath[0] |
| 148 | + firstNibble := firstByte >> 4 |
| 149 | + // 2 or 3 indicates leaf, while 0 or 1 indicates extension nodes in the Ethereum MPT specification. |
| 150 | + if firstNibble > 3 { |
| 151 | + return false, errors.New("first nibble cannot be greater than 3") |
| 152 | + } |
| 153 | + return firstNibble >= 2, nil |
| 154 | +} |
| 155 | + |
| 156 | +func decodeReceipt(data []byte) (*types.Receipt, error) { |
| 157 | + if len(data) == 0 { |
| 158 | + return nil, errors.New("empty data cannot be decoded into receipt") |
| 159 | + } |
| 160 | + rpt := new(types.Receipt) |
| 161 | + if err := rpt.UnmarshalBinary(data); err != nil { |
| 162 | + return nil, err |
| 163 | + } |
| 164 | + return rpt, nil |
| 165 | +} |
0 commit comments