Skip to content

Commit

Permalink
feat(rpc): adds hashi_getReceiptProof
Browse files Browse the repository at this point in the history
  • Loading branch information
allemanfredi committed Oct 31, 2024
1 parent 49b7f42 commit fcc5254
Show file tree
Hide file tree
Showing 10 changed files with 228 additions and 70 deletions.
2 changes: 1 addition & 1 deletion packages/rpc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,4 @@ To stop the running container:

```sh
docker stop [CONTAINER_ID or CONTAINER_NAME]
```
```
1 change: 1 addition & 0 deletions packages/rpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@ethereumjs/block": "^5.3.0",
"@ethereumjs/common": "^4.4.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethereumjs/trie": "^6.0.1",
"@ethereumjs/util": "^9.0.3",
"@gnosis/hashi-common": "0.1.0",
"body-parser": "^1.20.2",
Expand Down
2 changes: 2 additions & 0 deletions packages/rpc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { logger } from "@gnosis/hashi-common"

import logMiddleware from "./middlewares/log"
import getAccountAndStorageProof from "./methods/get-account-and-storage-proof"
import getReceiptProof from "./methods/get-receipt-proof"
import { Methods } from "./methods/types"

const start = async () => {
const server: TypedJSONRPCServer<Methods> = new JSONRPCServer()
server.addMethod("hashi_getAccountAndStorageProof", getAccountAndStorageProof)
server.addMethod("hashi_getReceiptProof", getReceiptProof)
server.applyMiddleware(logMiddleware)

const app = express()
Expand Down
75 changes: 7 additions & 68 deletions packages/rpc/src/methods/get-account-and-storage-proof.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,13 @@
import "dotenv/config"
import { ethers } from "ethers"
import { logger } from "@gnosis/hashi-common"
import { BlockHeader, JsonRpcBlock } from "@ethereumjs/block"
import { Common, Hardfork } from "@ethereumjs/common"
import {
bigIntToHex,
bytesToHex,
intToHex,
} from "@ethereumjs/util"
import { BlockHeader } from "@ethereumjs/block"
import { bigIntToHex, bytesToHex, intToHex } from "@ethereumjs/util"

import { GetAccountAndStorageProofParams, GetAccountAndStorageProofResponse } from "../types"

export function blockHeaderFromRpc(_block: JsonRpcBlock) {
const {
parentHash,
sha3Uncles,
miner,
stateRoot,
transactionsRoot,
receiptsRoot,
logsBloom,
difficulty,
number,
gasLimit,
gasUsed,
timestamp,
extraData,
mixHash,
nonce,
baseFeePerGas,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
} = _block
import { blockHeaderFromRpc } from "../utils/block"
import getCommon from "../utils/common"

return {
parentHash,
uncleHash: sha3Uncles,
coinbase: miner,
stateRoot,
transactionsTrie: transactionsRoot,
receiptTrie: receiptsRoot,
logsBloom,
difficulty,
number,
gasLimit,
gasUsed,
timestamp,
extraData,
mixHash,
nonce,
baseFeePerGas,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
}
}
import { GetAccountAndStorageProofParams, GetAccountAndStorageProofResponse } from "../types"

const getAccountAndStorageProof = async ({
address,
Expand All @@ -71,19 +19,10 @@ const getAccountAndStorageProof = async ({
try {
const rpcUrl = process.env[`JSON_RPC_URL_${chainId}`]
if (!rpcUrl) throw new Error("Chain not supported")
const provider = new ethers.JsonRpcProvider(rpcUrl)

const common = Common.custom(
{
chainId,
},
{
hardfork: Hardfork.Cancun,
eips: [1559, 4895, 4844, 4788],
},
)
const common = getCommon(chainId)

const provider = new ethers.JsonRpcProvider(rpcUrl)

const [proof, block] = await Promise.all([
provider.send("eth_getProof", [address, storageKeys, bigIntToHex(BigInt(ancestralBlockNumber || blockNumber))]),
provider.send("eth_getBlockByNumber", [intToHex(blockNumber), false]),
Expand Down
126 changes: 126 additions & 0 deletions packages/rpc/src/methods/get-receipt-proof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import "dotenv/config"
import { ethers, Log } from "ethers"
import { logger } from "@gnosis/hashi-common"
import { Trie } from "@ethereumjs/trie"
import { bytesToHex, concatBytes, hexToBigInt, hexToBytes, intToBytes, bytesToInt, intToHex } from "@ethereumjs/util"
import { RLP } from "@ethereumjs/rlp"
import { BlockHeader } from "@ethereumjs/block"

import { blockHeaderFromRpc } from "../utils/block"
import getCommon from "../utils/common"

import { GetReceiptProofParams, GetReceiptProofResponse } from "../types"

const encodeIndex = (_index: `0x${string}`) =>
_index === "0x0" ? RLP.encode(Buffer.alloc(0)) : RLP.encode(bytesToInt(hexToBytes(_index ?? "0x0")))

const getReceiptProof = async ({ logIndex, blockNumber, chainId, transactionHash }: GetReceiptProofParams) => {
try {
const rpcUrl = process.env[`JSON_RPC_URL_${chainId}`]
if (!rpcUrl) throw new Error("Chain not supported")
const provider = new ethers.JsonRpcProvider(rpcUrl)

const common = getCommon(chainId)

const targetTransactionReceipt = await provider.send("eth_getTransactionReceipt", [transactionHash])
if (!targetTransactionReceipt) throw new Error("Receipt not found")

const hexLogIndex = intToHex(logIndex)
const log = targetTransactionReceipt.logs.find((_log: any) => _log.logIndex === hexLogIndex)

if (!log) throw new Error(`Log ${logIndex} not found within the specified transaction`)
const effectiveLogIndex = targetTransactionReceipt.logs.findIndex((_log: any) => _log.logIndex === hexLogIndex)

const blockContainingLog = await provider.send("eth_getBlockByHash", [targetTransactionReceipt.blockHash, true])
const blockHeader = BlockHeader.fromHeaderData(blockHeaderFromRpc(blockContainingLog), { common })
const blockContainingLogNumber = Number(hexToBigInt(blockContainingLog.number))
if (blockNumber && blockNumber <= blockContainingLogNumber)
throw new Error("blockNumber must be greater than transaction.blockNumber")

let receipts = []
for (const transaction of blockContainingLog.transactions) {
receipts.push(await provider.send("eth_getTransactionReceipt", [transaction.hash]))
}

const encodedReceipts = receipts.map((_receipt, _index) => {
let type = Number(hexToBigInt(_receipt.type))
if (type === 126) {
// Optimism DepositTxType
const encoded = RLP.encode([
_receipt.status === "0x1" ? "0x1" : "0x",
_receipt.cumulativeGasUsed,
_receipt.logsBloom,
_receipt.logs.map((_log: Log) => {
return [_log.address, _log.topics, _log.data]
}),
_receipt.depositNonce,
_receipt.depositReceiptVersion,
])
return concatBytes(intToBytes(126), encoded)
}

const encoded = RLP.encode([
_receipt.status === "0x1" ? hexToBytes("0x01") : Uint8Array.from([]),
hexToBytes(_receipt.cumulativeGasUsed),
hexToBytes(_receipt.logsBloom),
_receipt.logs.map((_log: Log) => {
return [hexToBytes(_log.address), _log.topics.map(hexToBytes), hexToBytes(_log.data)]
}),
])
if (type === 0) return encoded
return concatBytes(intToBytes(type), encoded)
})

const trie = new Trie()
await Promise.all(
receipts.map((_receipt, _index) => trie.put(encodeIndex(_receipt.transactionIndex), encodedReceipts[_index])),
)

//
const root = bytesToHex(trie.root())
if (root !== blockContainingLog.receiptsRoot) {
throw Error("The trie.root() and blockContainingLog.receiptsRoot do not match")
}

// If a blockNumber is specified, we need to retrieve the chain from blockNumber to blockContainingLog.number, as this indicates that
// Hashi has the block number, and the block where the transaction occurred is an ancestor of blockNumber.
let ancestralBlockHeaders = [] as `0x${string}`[]
if (blockNumber) {
let ancestralBlockNumber = Number(hexToBigInt(blockContainingLog.number))
if (ancestralBlockNumber >= blockNumber) throw new Error("Invalid ancestral blockContainingLog number")

const blockNumbers = [...Array(blockNumber - ancestralBlockNumber + 1).keys()].map(
(num) => num + ancestralBlockNumber,
)
ancestralBlockHeaders = (
await Promise.all(
blockNumbers.map((_blockNumber) => provider.send("eth_getBlockByNumber", [intToHex(_blockNumber), false])),
)
)
.map((_block) => bytesToHex(BlockHeader.fromHeaderData(blockHeaderFromRpc(_block), { common }).serialize()))
.reverse()
}

const receiptKey = encodeIndex(targetTransactionReceipt!.transactionIndex)
const proof = await trie.createProof(receiptKey)

console.log(log)
return {
proof: [
chainId,
blockNumber ? blockNumber : blockContainingLogNumber,
bytesToHex(blockHeader.serialize()),
blockNumber ? blockContainingLogNumber : 0,
ancestralBlockHeaders,
proof.map(bytesToHex),
bytesToHex(encodeIndex(targetTransactionReceipt.transactionIndex)),
effectiveLogIndex
],
} as GetReceiptProofResponse
} catch (_err) {
logger.error(_err)
throw _err
}
}

export default getReceiptProof
1 change: 1 addition & 0 deletions packages/rpc/src/methods/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { GetAccountAndStorageProofParams, GetAccountAndStorageProofResponse } fr

export type Methods = {
hashi_getAccountAndStorageProof(params: GetAccountAndStorageProofParams): GetAccountAndStorageProofResponse
hashi_getReceiptProof(params: GetReceiptProofParams): GetReceiptProofResponse
}
22 changes: 22 additions & 0 deletions packages/rpc/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ export type AccountAndStorageProof = [
`0x${string}`,
]

export type ReceiptProof = [
number,
number,
`0x${string}`,
number,
`0x${string}`[],
`0x${string}`[],
`0x${string}`,
number
]

export type GetAccountAndStorageProofParams = {
address: `0x${string}`
ancestralBlockNumber?: number
Expand All @@ -22,3 +33,14 @@ export type GetAccountAndStorageProofParams = {
export type GetAccountAndStorageProofResponse = {
proof: AccountAndStorageProof
}

export type GetReceiptProofParams = {
blockNumber: number
chainId: number
logIndex: number
transactionHash: `0x${string}`
}

export type GetReceiptProofResponse = {
proof: ReceiptProof
}
51 changes: 51 additions & 0 deletions packages/rpc/src/utils/block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { JsonRpcBlock } from "@ethereumjs/block"

export const blockHeaderFromRpc = (_block: JsonRpcBlock) => {
const {
parentHash,
sha3Uncles,
miner,
stateRoot,
transactionsRoot,
receiptsRoot,
logsBloom,
difficulty,
number,
gasLimit,
gasUsed,
timestamp,
extraData,
mixHash,
nonce,
baseFeePerGas,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
} = _block

return {
parentHash,
uncleHash: sha3Uncles,
coinbase: miner,
stateRoot,
transactionsTrie: transactionsRoot,
receiptTrie: receiptsRoot,
logsBloom,
difficulty,
number,
gasLimit,
gasUsed,
timestamp,
extraData,
mixHash,
nonce,
baseFeePerGas,
withdrawalsRoot,
blobGasUsed,
excessBlobGas,
parentBeaconBlockRoot,
requestsRoot,
}
}
16 changes: 16 additions & 0 deletions packages/rpc/src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import "dotenv/config"

import { Common, Hardfork } from "@ethereumjs/common"

const getCommon = (_chainId: number) =>
Common.custom(
{
chainId: _chainId,
},
{
hardfork: Hardfork.Cancun,
eips: [1559, 4895, 4844, 4788],
},
)

export default getCommon
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@
resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842"
integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==

"@ethereumjs/trie@^6.2.1":
"@ethereumjs/trie@^6.0.1", "@ethereumjs/trie@^6.2.1":
version "6.2.1"
resolved "https://registry.yarnpkg.com/@ethereumjs/trie/-/trie-6.2.1.tgz#11d3e91ffd7d565f468a62c0e3d7952063991fa9"
integrity sha512-MguABMVi/dPtgagK+SuY57rpXFP+Ghr2x+pBDy+e3VmMqUY+WGzFu1QWjBb5/iJ7lINk4CI2Uwsih07Nu9sTSg==
Expand Down

0 comments on commit fcc5254

Please sign in to comment.