Commit 0bf3b9b4 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

contracts-bedrock: differential fuzzing (#2980)

* core-utils: add encoding and hashing functions to core-utils

* ci: update

* contracts-bedrock: differential fuzzing

* deps: update forge-std

* contracts-bedrock: set fuzz runs to 512

* contracts-bedrock: rename differential-testing method

* contracts-bedrock: no sender as address(OptimismPortal)
parent b7087214
---
'@eth-optimism/core-utils': patch
---
Add encoding and hashing functions for bedrock
---
'@eth-optimism/contracts-bedrock': patch
'@eth-optimism/contracts-periphery': patch
---
Update forge-std
...@@ -107,7 +107,7 @@ jobs: ...@@ -107,7 +107,7 @@ jobs:
contracts-bedrock-tests: contracts-bedrock-tests:
docker: docker:
- image: ethereumoptimism/ci-builder:latest - image: ethereumoptimism/ci-builder:latest
resource_class: medium resource_class: large
steps: steps:
- restore_cache: - restore_cache:
keys: keys:
...@@ -135,11 +135,15 @@ jobs: ...@@ -135,11 +135,15 @@ jobs:
name: test name: test
command: yarn test command: yarn test
working_directory: packages/contracts-bedrock working_directory: packages/contracts-bedrock
environment:
FOUNDRY_PROFILE: ci
- run: - run:
name: gas snapshot name: gas snapshot
command: | command: |
forge --version forge --version
forge snapshot --check || exit 0 forge snapshot --check || exit 0
environment:
FOUNDRY_PROFILE: ci
working_directory: packages/contracts-bedrock working_directory: packages/contracts-bedrock
- run: - run:
name: storage snapshot name: storage snapshot
......
This diff is collapsed.
//SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity 0.8.10; pragma solidity 0.8.10;
import { CommonTest } from "./CommonTest.t.sol"; import { CommonTest } from "./CommonTest.t.sol";
import { Encoding } from "../libraries/Encoding.sol"; import { Encoding } from "../libraries/Encoding.sol";
contract Encoding_Test is CommonTest { contract Encoding_Test is CommonTest {
function setUp() external {
_setUp();
}
function test_nonceVersioning(uint240 _nonce, uint16 _version) external { function test_nonceVersioning(uint240 _nonce, uint16 _version) external {
(uint240 nonce, uint16 version) = Encoding.decodeVersionedNonce( (uint240 nonce, uint16 version) = Encoding.decodeVersionedNonce(
Encoding.encodeVersionedNonce(_nonce, _version) Encoding.encodeVersionedNonce(_nonce, _version)
...@@ -12,4 +16,52 @@ contract Encoding_Test is CommonTest { ...@@ -12,4 +16,52 @@ contract Encoding_Test is CommonTest {
assertEq(version, _version); assertEq(version, _version);
assertEq(nonce, _nonce); assertEq(nonce, _nonce);
} }
function test_decodeVersionedNonce_differential(uint240 _nonce, uint16 _version) external {
uint256 nonce = uint256(Encoding.encodeVersionedNonce(_nonce, _version));
(uint256 decodedNonce, uint256 decodedVersion) = ffi.decodeVersionedNonce(nonce);
assertEq(
_version,
uint16(decodedVersion)
);
assertEq(
_nonce,
uint240(decodedNonce)
);
}
function test_encodeCrossDomainMessage_differential(
uint240 _nonce,
uint8 _version,
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
) external {
uint8 version = _version % 2;
uint256 nonce = Encoding.encodeVersionedNonce(_nonce, version);
bytes memory encoding = Encoding.encodeCrossDomainMessage(
nonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
bytes memory _encoding = ffi.encodeCrossDomainMessage(
nonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
assertEq(encoding, _encoding);
}
} }
//SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity 0.8.10; pragma solidity 0.8.10;
import { CommonTest } from "./CommonTest.t.sol"; import { CommonTest } from "./CommonTest.t.sol";
...@@ -6,6 +6,10 @@ import { Hashing } from "../libraries/Hashing.sol"; ...@@ -6,6 +6,10 @@ import { Hashing } from "../libraries/Hashing.sol";
import { Encoding } from "../libraries/Encoding.sol"; import { Encoding } from "../libraries/Encoding.sol";
contract Hashing_Test is CommonTest { contract Hashing_Test is CommonTest {
function setUp() external {
_setUp();
}
function test_hashDepositSource() external { function test_hashDepositSource() external {
bytes32 sourceHash = Hashing.hashDepositSource( bytes32 sourceHash = Hashing.hashDepositSource(
0xd25df7858efc1778118fb133ac561b138845361626dfb976699c5287ed0f4959, 0xd25df7858efc1778118fb133ac561b138845361626dfb976699c5287ed0f4959,
...@@ -17,4 +21,127 @@ contract Hashing_Test is CommonTest { ...@@ -17,4 +21,127 @@ contract Hashing_Test is CommonTest {
0xf923fb07134d7d287cb52c770cc619e17e82606c21a875c92f4c63b65280a5cc 0xf923fb07134d7d287cb52c770cc619e17e82606c21a875c92f4c63b65280a5cc
); );
} }
function test_hashCrossDomainMessage_differential(
uint256 _nonce,
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
) external {
// Discard any fuzz tests with an invalid version
(, uint16 version) = Encoding.decodeVersionedNonce(_nonce);
vm.assume(version < 2);
bytes32 _hash = ffi.hashCrossDomainMessage(
_nonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
bytes32 hash = Hashing.hashCrossDomainMessage(
_nonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
assertEq(hash, _hash);
}
function test_hashWithdrawal_differential(
uint256 _nonce,
address _sender,
address _target,
uint256 _value,
uint256 _gasLimit,
bytes memory _data
) external {
bytes32 hash = Hashing.hashWithdrawal(
_nonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
bytes32 _hash = ffi.hashWithdrawal(
_nonce,
_sender,
_target,
_value,
_gasLimit,
_data
);
assertEq(hash, _hash);
}
function test_hashOutputRootProof_differential(
bytes32 _version,
bytes32 _stateRoot,
bytes32 _withdrawerStorageRoot,
bytes32 _latestBlockhash
) external {
Hashing.OutputRootProof memory proof = Hashing.OutputRootProof({
version: _version,
stateRoot: _stateRoot,
withdrawerStorageRoot: _withdrawerStorageRoot,
latestBlockhash: _latestBlockhash
});
bytes32 hash = Hashing.hashOutputRootProof(proof);
bytes32 _hash = ffi.hashOutputRootProof(
_version,
_stateRoot,
_withdrawerStorageRoot,
_latestBlockhash
);
assertEq(hash, _hash);
}
// TODO(tynes): foundry bug cannot serialize
// bytes32 as strings with vm.toString
function test_hashDepositTransaction_differential(
address _from,
address _to,
uint256 _mint,
uint256 _value,
uint64 _gas,
bytes memory _data,
uint256 _logIndex
) external {
bytes32 hash = Hashing.hashDepositTransaction(
_from,
_to,
_value,
_mint,
_gas,
false, // isCreate
_data,
bytes32(uint256(0)),
_logIndex
);
bytes32 _hash = ffi.hashDepositTransaction(
_from,
_to,
_mint,
_value,
_gas,
_data,
_logIndex
);
assertEq(hash, _hash);
}
} }
//SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
pragma solidity 0.8.10; pragma solidity 0.8.10;
import { Portal_Initializer, CommonTest, NextImpl } from "./CommonTest.t.sol"; import { Portal_Initializer, CommonTest, NextImpl } from "./CommonTest.t.sol";
import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol"; import { AddressAliasHelper } from "../vendor/AddressAliasHelper.sol";
import { L2OutputOracle } from "../L1/L2OutputOracle.sol"; import { L2OutputOracle } from "../L1/L2OutputOracle.sol";
import { OptimismPortal } from "../L1/OptimismPortal.sol"; import { OptimismPortal } from "../L1/OptimismPortal.sol";
...@@ -30,8 +29,6 @@ contract OptimismPortal_Test is Portal_Initializer { ...@@ -30,8 +29,6 @@ contract OptimismPortal_Test is Portal_Initializer {
assertEq(address(op).balance, 100); assertEq(address(op).balance, 100);
} }
// function test_OptimismPortalDepositTransaction() external {}
// Test: depositTransaction fails when contract creation has a non-zero destination address // Test: depositTransaction fails when contract creation has a non-zero destination address
function test_OptimismPortalContractCreationReverts() external { function test_OptimismPortalContractCreationReverts() external {
// contract creation must have a target of address(0) // contract creation must have a target of address(0)
...@@ -314,6 +311,84 @@ contract OptimismPortal_Test is Portal_Initializer { ...@@ -314,6 +311,84 @@ contract OptimismPortal_Test is Portal_Initializer {
vm.expectRevert("L2OutputOracle: No output found for that block number."); vm.expectRevert("L2OutputOracle: No output found for that block number.");
assertEq(op.isBlockFinalized(checkpoint + 1), false); assertEq(op.isBlockFinalized(checkpoint + 1), false);
} }
function test_finalizeWithdrawalTransaction_differential(
address _sender,
address _target,
uint64 _value,
uint8 _gasLimit,
bytes memory _data
) external {
// Cannot call the optimism portal
vm.assume(_target != address(op));
uint256 _nonce = messagePasser.nonce();
(
bytes32 stateRoot,
bytes32 storageRoot,
bytes32 outputRoot,
bytes32 withdrawalHash,
bytes memory withdrawalProof
) = ffi.getFinalizeWithdrawalTransactionInputs(
_nonce,
_sender,
_target,
_value,
uint256(_gasLimit),
_data
);
Hashing.OutputRootProof memory proof = Hashing.OutputRootProof({
version: bytes32(uint256(0)),
stateRoot: stateRoot,
withdrawerStorageRoot: storageRoot,
latestBlockhash: bytes32(uint256(0))
});
// Ensure the values returned from ffi are correct
assertEq(outputRoot, Hashing.hashOutputRootProof(proof));
assertEq(withdrawalHash, Hashing.hashWithdrawal(
_nonce,
_sender,
_target,
_value,
uint64(_gasLimit),
_data
));
// Mock the call to the oracle
vm.mockCall(
address(oracle),
abi.encodeWithSelector(oracle.getL2Output.selector),
abi.encode(outputRoot, 0)
);
// Start the withdrawal, it must be initiated by the _sender and the
// correct value must be passed along
vm.deal(_sender, _value);
vm.prank(_sender);
messagePasser.initiateWithdrawal{ value: _value }(
_target,
uint256(_gasLimit),
_data
);
// Ensure that the sentMessages is correct
assertEq(messagePasser.sentMessages(withdrawalHash), true);
vm.warp(op.FINALIZATION_PERIOD_SECONDS() + 1);
op.finalizeWithdrawalTransaction{ value: _value }(
messagePasser.nonce() - 1,
_sender,
_target,
_value,
uint64(_gasLimit),
_data,
100, // l2BlockNumber
proof,
withdrawalProof
);
}
} }
contract OptimismPortalUpgradeable_Test is Portal_Initializer { contract OptimismPortalUpgradeable_Test is Portal_Initializer {
......
...@@ -16,3 +16,8 @@ remappings = [ ...@@ -16,3 +16,8 @@ remappings = [
extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout']
bytecode_hash = 'none' bytecode_hash = 'none'
build_info = true build_info = true
ffi = true
fuzz_runs = 16
[ci]
fuzz_runs = 512
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
"build": "hardhat compile && yarn build:ts && yarn typechain", "build": "hardhat compile && yarn build:ts && yarn typechain",
"build:ts": "tsc -p tsconfig.json", "build:ts": "tsc -p tsconfig.json",
"deploy": "hardhat deploy", "deploy": "hardhat deploy",
"test": "forge test", "test": "yarn build:ts && forge test",
"gas-snapshot": "forge snapshot", "gas-snapshot": "forge snapshot",
"storage-snapshot": "./scripts/storage-snapshot.sh", "storage-snapshot": "./scripts/storage-snapshot.sh",
"slither": "./scripts/slither.sh", "slither": "./scripts/slither.sh",
...@@ -35,6 +35,8 @@ ...@@ -35,6 +35,8 @@
}, },
"dependencies": { "dependencies": {
"@eth-optimism/core-utils": "^0.9.1", "@eth-optimism/core-utils": "^0.9.1",
"@ethereumjs/trie": "^5.0.0-beta.1",
"@ethereumjs/util": "^8.0.0-beta.1",
"@openzeppelin/contracts": "^4.5.0", "@openzeppelin/contracts": "^4.5.0",
"@openzeppelin/contracts-upgradeable": "^4.5.2", "@openzeppelin/contracts-upgradeable": "^4.5.2",
"@rari-capital/solmate": "https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc", "@rari-capital/solmate": "https://github.com/rari-capital/solmate.git#8f9b23f8838670afda0fd8983f2c41e8037ae6bc",
...@@ -43,7 +45,7 @@ ...@@ -43,7 +45,7 @@
"ethereumjs-wallet": "^1.0.2", "ethereumjs-wallet": "^1.0.2",
"ethers": "^5.6.8", "ethers": "^5.6.8",
"excessively-safe-call": "https://github.com/nomad-xyz/ExcessivelySafeCall.git#4fcdfd3593d21381f696c790fa6180b8ef559c1e", "excessively-safe-call": "https://github.com/nomad-xyz/ExcessivelySafeCall.git#4fcdfd3593d21381f696c790fa6180b8ef559c1e",
"forge-std": "https://github.com/foundry-rs/forge-std.git#62caef29b0f87a2c6aaaf634b2ca4c09b6867c92", "forge-std": "https://github.com/foundry-rs/forge-std.git#f18682b2874fc57d7c80a511fed0b35ec4201ffa",
"hardhat": "^2.9.6", "hardhat": "^2.9.6",
"merkle-patricia-tree": "^4.2.4", "merkle-patricia-tree": "^4.2.4",
"rlp": "^2.2.7" "rlp": "^2.2.7"
......
import { BigNumber, utils, constants } from 'ethers'
import {
decodeVersionedNonce,
hashCrossDomainMessage,
DepositTx,
SourceHashDomain,
encodeCrossDomainMessage,
hashWithdrawal,
hashOutputRootProof,
} from '@eth-optimism/core-utils'
import { SecureTrie } from '@ethereumjs/trie'
import { Account, Address, toBuffer, bufferToHex } from '@ethereumjs/util'
import { predeploys } from '../src'
const { hexZeroPad, RLP, keccak256 } = utils
const args = process.argv.slice(2)
const command = args[0]
;(async () => {
switch (command) {
case 'decodeVersionedNonce': {
const input = BigNumber.from(args[1])
const [nonce, version] = decodeVersionedNonce(input)
const output = utils.defaultAbiCoder.encode(
['uint256', 'uint256'],
[nonce.toHexString(), version.toHexString()]
)
process.stdout.write(output)
break
}
case 'encodeCrossDomainMessage': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gasLimit = BigNumber.from(args[5])
const data = args[6]
const encoding = encodeCrossDomainMessage(
nonce,
sender,
target,
value,
gasLimit,
data
)
const output = utils.defaultAbiCoder.encode(['bytes'], [encoding])
process.stdout.write(output)
break
}
case 'hashCrossDomainMessage': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gasLimit = BigNumber.from(args[5])
const data = args[6]
const hash = hashCrossDomainMessage(
nonce,
sender,
target,
value,
gasLimit,
data
)
const output = utils.defaultAbiCoder.encode(['bytes32'], [hash])
process.stdout.write(output)
break
}
case 'hashDepositTransaction': {
// The solidity transaction hash computation currently only works with
// user deposits. System deposit transaction hashing is not supported.
const l1BlockHash = args[1]
const logIndex = BigNumber.from(args[2])
const from = args[3]
const to = args[4]
const mint = BigNumber.from(args[5])
const value = BigNumber.from(args[6])
const gas = BigNumber.from(args[7])
const data = args[8]
const tx = new DepositTx({
l1BlockHash,
logIndex,
from,
to,
mint,
value,
gas,
data,
domain: SourceHashDomain.UserDeposit,
})
const digest = tx.hash()
const output = utils.defaultAbiCoder.encode(['bytes32'], [digest])
process.stdout.write(output)
break
}
case 'hashWithdrawal': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gas = BigNumber.from(args[5])
const data = args[6]
const hash = hashWithdrawal(nonce, sender, target, value, gas, data)
const output = utils.defaultAbiCoder.encode(['bytes32'], [hash])
process.stdout.write(output)
break
}
case 'hashOutputRootProof': {
const version = hexZeroPad(BigNumber.from(args[1]).toHexString(), 32)
const stateRoot = hexZeroPad(BigNumber.from(args[2]).toHexString(), 32)
const withdrawerStorageRoot = hexZeroPad(
BigNumber.from(args[3]).toHexString(),
32
)
const latestBlockhash = hexZeroPad(
BigNumber.from(args[4]).toHexString(),
32
)
const hash = hashOutputRootProof({
version,
stateRoot,
withdrawerStorageRoot,
latestBlockhash,
})
const output = utils.defaultAbiCoder.encode(['bytes32'], [hash])
process.stdout.write(output)
break
}
case 'getFinalizeWithdrawalTransactionInputs': {
const nonce = BigNumber.from(args[1])
const sender = args[2]
const target = args[3]
const value = BigNumber.from(args[4])
const gas = BigNumber.from(args[5])
const data = args[6]
// Compute the withdrawalHash
const withdrawalHash = hashWithdrawal(
nonce,
sender,
target,
value,
gas,
data
)
// Compute the storage slot the withdrawalHash will be stored in
const slot = utils.defaultAbiCoder.encode(
['bytes32', 'bytes32'],
[withdrawalHash, utils.hexZeroPad('0x', 32)]
)
const key = keccak256(slot)
// Create the account storage trie
const storage = new SecureTrie()
// Put a bool "true" into storage
await storage.put(toBuffer(key), toBuffer('0x01'))
// Put the storage root into the L2ToL1MessagePasser storage
const address = Address.fromString(predeploys.L2ToL1MessagePasser)
const account = Account.fromAccountData({
nonce: 0,
balance: 0,
stateRoot: storage.root,
})
const world = new SecureTrie()
await world.put(address.toBuffer(), account.serialize())
const proof = await SecureTrie.createProof(storage, toBuffer(key))
const outputRoot = hashOutputRootProof({
version: constants.HashZero,
stateRoot: bufferToHex(world.root),
withdrawerStorageRoot: bufferToHex(storage.root),
latestBlockhash: constants.HashZero,
})
const encodedProof = RLP.encode(proof)
const output = utils.defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'bytes32', 'bytes'],
[world.root, storage.root, outputRoot, withdrawalHash, encodedProof]
)
process.stdout.write(output)
break
}
}
})().catch((err: Error) => {
console.error(err)
process.stdout.write('')
})
// Script for generating an inclusion proof for use in testing
// Intended for use with forge test --ffi, accepts abi encoded input and returns
// only the storageTrieWitness.
import { generateMockWithdrawalProof } from '../helpers'
let args = process.argv.slice(2)[0]
args = args
.replace('0x', '')
.split('')
.filter((char) => '0123456789abcdef'.includes(char))
.join('')
const main = async () => {
const proof = await generateMockWithdrawalProof('0x' + args)
console.log(proof.storageTrieWitness.slice(2))
}
main()
// Script for generating an inclusion proof for use in testing.
// Meant for manual usage, ie.
// ts-node scripts/makeProof.ts 1 0x0000000000000000000000000000000000000002 0x0000000000000000000000000000000000000003 4 500000 0x06
import { generateMockWithdrawalProof } from '../helpers'
const args = process.argv.slice(2)
const [nonce, sender, target, value, gasLimit, data] = args
const main = async () => {
const proof = await generateMockWithdrawalProof({
nonce: +nonce,
sender,
target,
value: +value,
gasLimit: +gasLimit,
data,
})
console.log(proof)
}
main()
import { ethers } from 'ethers'
import { toHexString } from '@eth-optimism/core-utils'
import { TrieTestGenerator } from './trie-test-generator'
import { predeploys } from './constants'
interface WithdrawalArgs {
nonce: number
sender: string
target: string
value: number
gasLimit: number
data: string
}
interface OutputRootProof {
version: string
stateRoot: string
withdrawerStorageRoot: string
latestBlockhash: string
}
export const deriveWithdrawalHash = (wd: WithdrawalArgs): string => {
return ethers.utils.keccak256(
ethers.utils.defaultAbiCoder.encode(
['uint256', 'address', 'address', 'uint256', 'uint256', 'bytes'],
[wd.nonce, wd.sender, wd.target, wd.value, wd.gasLimit, wd.data]
)
)
}
export const generateMockWithdrawalProof = async (
wd: WithdrawalArgs | string
): Promise<{
outputRootProof: OutputRootProof
storageTrieWitness: string
}> => {
let withdrawalHash
if (typeof wd == 'string') {
// wd should be an abi encoded string
withdrawalHash = ethers.utils.keccak256(wd)
} else {
withdrawalHash = deriveWithdrawalHash(wd as WithdrawalArgs)
}
const storageKey = ethers.utils.keccak256(
ethers.utils.hexConcat([
withdrawalHash,
ethers.utils.hexZeroPad('0x01', 32),
])
)
const storageGenerator = await TrieTestGenerator.fromNodes({
nodes: [
{
key: storageKey,
val: '0x' + '01'.padStart(2, '0'),
},
],
secure: true,
})
const generator = await TrieTestGenerator.fromAccounts({
accounts: [
{
address: predeploys.L2ToL1MessagePasser,
nonce: 0,
balance: 0,
codeHash: ethers.utils.keccak256('0x1234'),
storageRoot: toHexString(storageGenerator._trie.root),
},
],
secure: true,
})
return {
outputRootProof: {
version: ethers.constants.HashZero,
stateRoot: toHexString(generator._trie.root),
withdrawerStorageRoot: toHexString(storageGenerator._trie.root),
latestBlockhash: ethers.constants.HashZero,
},
storageTrieWitness: (
await storageGenerator.makeInclusionProofTest(storageKey)
).proof,
}
}
export const generateOutputRoot = (outputElements: {
version: string
stateRoot: string
withdrawerStorageRoot: string
latestBlockhash: string
}) => {
const { version, stateRoot, withdrawerStorageRoot, latestBlockhash } =
outputElements
return ethers.utils.solidityKeccak256(
['bytes32', 'bytes32', 'bytes32', 'bytes32'],
[version, stateRoot, withdrawerStorageRoot, latestBlockhash]
)
}
export * from './generateProofs'
export * from './constants' export * from './constants'
/* External Imports */
import * as rlp from 'rlp'
// import { default as seedbytes } from 'random-bytes-seed'
import { SecureTrie, BaseTrie } from 'merkle-patricia-tree'
import { fromHexString, toHexString } from '@eth-optimism/core-utils'
import { ethers } from 'ethers'
interface TrieNode {
key: string
val: string
}
interface InclusionProofTest {
key: string
val: string
proof: string
root: string
}
interface NodeUpdateTest extends InclusionProofTest {
newRoot: string
}
interface EthereumAccount {
address?: string
nonce: number
balance: number
codeHash: string
storageRoot?: string
storage?: TrieNode[]
}
interface AccountProofTest {
address: string
account: EthereumAccount
accountTrieWitness: string
accountTrieRoot: string
}
interface AccountUpdateTest extends AccountProofTest {
newAccountTrieRoot: string
}
const rlpEncodeAccount = (account: EthereumAccount): string => {
return toHexString(
rlp.encode([
account.nonce,
account.balance,
account.storageRoot || ethers.constants.HashZero,
account.codeHash || ethers.constants.HashZero,
])
)
}
const rlpDecodeAccount = (encoded: string): EthereumAccount => {
const decoded = rlp.decode(fromHexString(encoded)) as any
return {
nonce: decoded[0].length ? parseInt(decoded[0], 16) : 0,
balance: decoded[1].length ? parseInt(decoded[1], 16) : 0,
storageRoot: decoded[2].length
? toHexString(decoded[2])
: ethers.constants.HashZero,
codeHash: decoded[3].length
? toHexString(decoded[3])
: ethers.constants.HashZero,
}
}
const makeTrie = async (
nodes: TrieNode[],
secure?: boolean
): Promise<{
trie: SecureTrie | BaseTrie
TrieClass: any
}> => {
const TrieClass = secure ? SecureTrie : BaseTrie
const trie = new TrieClass()
for (const node of nodes) {
await trie.put(fromHexString(node.key), fromHexString(node.val))
}
return {
trie,
TrieClass,
}
}
export class TrieTestGenerator {
constructor(
public _TrieClass: any,
public _trie: SecureTrie | BaseTrie,
public _nodes: TrieNode[],
public _subGenerators?: TrieTestGenerator[]
) {}
static async fromNodes(opts: {
nodes: TrieNode[]
secure?: boolean
}): Promise<TrieTestGenerator> {
const { trie, TrieClass } = await makeTrie(opts.nodes, opts.secure)
return new TrieTestGenerator(TrieClass, trie, opts.nodes)
}
// static async fromRandom(opts: {
// seed: string
// nodeCount: number
// secure?: boolean
// keySize?: number
// valSize?: number
// }): Promise<TrieTestGenerator> {
// const getRandomBytes = seedbytes(opts.seed)
// const nodes: TrieNode[] = [...Array(opts.nodeCount)].map(() => {
// return {
// key: toHexString(getRandomBytes(opts.keySize || 32)),
// val: toHexString(getRandomBytes(opts.valSize || 32)),
// }
// })
// return TrieTestGenerator.fromNodes({
// nodes,
// secure: opts.secure,
// })
// }
static async fromAccounts(opts: {
accounts: EthereumAccount[]
secure?: boolean
}): Promise<TrieTestGenerator> {
const subGenerators: TrieTestGenerator[] = []
for (const account of opts.accounts) {
if (account.storage) {
const subGenerator = await TrieTestGenerator.fromNodes({
nodes: account.storage,
secure: opts.secure,
})
account.storageRoot = toHexString(subGenerator._trie.root)
subGenerators.push(subGenerator)
}
}
const nodes = opts.accounts.map((account) => {
return {
key: account.address as string,
val: rlpEncodeAccount(account),
}
})
const { trie, TrieClass } = await makeTrie(nodes, opts.secure)
return new TrieTestGenerator(TrieClass, trie, nodes, subGenerators)
}
public async makeInclusionProofTest(
key: string | number
): Promise<InclusionProofTest> {
if (typeof key === 'number') {
key = this._nodes[key].key
}
const trie = this._trie.copy()
const proof = await this.prove(key)
const val = await trie.get(fromHexString(key))
return {
proof: toHexString(rlp.encode(proof)),
key: toHexString(key),
val: toHexString(val),
root: toHexString(trie.root),
}
}
public async makeAllInclusionProofTests(): Promise<InclusionProofTest[]> {
return Promise.all(
this._nodes.map(async (node) => {
return this.makeInclusionProofTest(node.key)
})
)
}
public async makeNodeUpdateTest(
key: string | number,
val: string
): Promise<NodeUpdateTest> {
if (typeof key === 'number') {
key = this._nodes[key].key
}
const trie = this._trie.copy()
const proof = await this.prove(key)
const oldRoot = trie.root
await trie.put(fromHexString(key), fromHexString(val))
const newRoot = trie.root
return {
proof: toHexString(rlp.encode(proof)),
key: toHexString(key),
val: toHexString(val),
root: toHexString(oldRoot),
newRoot: toHexString(newRoot),
}
}
public async makeAccountProofTest(
address: string | number
): Promise<AccountProofTest> {
if (typeof address === 'number') {
address = this._nodes[address].key
}
const trie = this._trie.copy()
const proof = await this.prove(address)
const account = await trie.get(fromHexString(address))
return {
address,
account: rlpDecodeAccount(toHexString(account)),
accountTrieWitness: toHexString(rlp.encode(proof)),
accountTrieRoot: toHexString(trie.root),
}
}
public async makeAccountUpdateTest(
address: string | number,
account: EthereumAccount
): Promise<AccountUpdateTest> {
if (typeof address === 'number') {
address = this._nodes[address].key
}
const trie = this._trie.copy()
const proof = await this.prove(address)
const oldRoot = trie.root
await trie.put(
fromHexString(address),
fromHexString(rlpEncodeAccount(account))
)
const newRoot = trie.root
return {
address,
account,
accountTrieWitness: toHexString(rlp.encode(proof)),
accountTrieRoot: toHexString(oldRoot),
newAccountTrieRoot: toHexString(newRoot),
}
}
private async prove(key: string): Promise<any> {
return this._TrieClass.prove(this._trie, fromHexString(key))
}
}
import { expect } from 'chai'
import { BigNumber } from 'ethers'
import { DepositTx, SourceHashDomain } from '../src'
describe('Helpers', () => {
describe('DepositTx', () => {
// TODO(tynes): this is out of date now that the subversion
// byte has been added
it('should serialize/deserialize and hash', () => {
// constants serialized using optimistic-geth
// TODO(tynes): more tests
const hash =
'0xf58e30138cb01330f6450b9a5e717a63840ad2e21f17340105b388ad3c668749'
const raw =
'0x7e00f862a0f923fb07134d7d287cb52c770cc619e17e82606c21a875c92f4c63b65280a5cc94f39fd6e51aad88f6f4ce6ab8827279cfffb9226694b79f76ef2c5f0286176833e7b2eee103b1cc3244880e043da617250000880de0b6b3a7640000832dc6c080'
const tx = new DepositTx({
from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
gas: '0x2dc6c0',
data: '0x',
to: '0xB79f76EF2c5F0286176833E7B2eEe103b1CC3244',
value: '0xde0b6b3a7640000',
domain: SourceHashDomain.UserDeposit,
l1BlockHash:
'0xd25df7858efc1778118fb133ac561b138845361626dfb976699c5287ed0f4959',
logIndex: 1,
mint: '0xe043da617250000',
})
const sourceHash = tx.sourceHash()
expect(sourceHash).to.deep.eq(
'0xf923fb07134d7d287cb52c770cc619e17e82606c21a875c92f4c63b65280a5cc'
)
const encoded = tx.encode()
expect(encoded).to.deep.eq(raw)
const hashed = tx.hash()
expect(hashed).to.deep.eq(hash)
const decoded = DepositTx.decode(raw, {
domain: SourceHashDomain.UserDeposit,
l1BlockHash: tx.l1BlockHash,
logIndex: tx.logIndex,
})
expect(decoded.from).to.deep.eq(tx.from)
expect(decoded.gas).to.deep.eq(BigNumber.from(tx.gas))
expect(decoded.data).to.deep.eq(tx.data)
expect(decoded.to).to.deep.eq(tx.to)
expect(decoded.value).to.deep.eq(BigNumber.from(tx.value))
expect(decoded.domain).to.deep.eq(SourceHashDomain.UserDeposit)
expect(decoded.l1BlockHash).to.deep.eq(tx.l1BlockHash)
expect(decoded.logIndex).to.deep.eq(tx.logIndex)
expect(decoded.mint).to.deep.eq(BigNumber.from(tx.mint))
})
})
})
{ {
"extends": "../../tsconfig.json", "extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"rootDir": "./src", "rootDir": ".",
"outDir": "./dist" "outDir": "./dist"
}, },
"exclude": ["hardhat.config.ts", "deploy", "tasks", "test"], "exclude": ["hardhat.config.ts", "deploy", "tasks", "test"],
"include": ["src/**/*"] "include": ["src/**/*", "scripts/differential-testing.ts"]
} }
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
"ds-test": "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5", "ds-test": "https://github.com/dapphub/ds-test.git#9310e879db8ba3ea6d5c6489a579118fd264a3f5",
"ethereum-waffle": "^3.4.4", "ethereum-waffle": "^3.4.4",
"ethers": "^5.6.8", "ethers": "^5.6.8",
"forge-std": "https://github.com/foundry-rs/forge-std.git#62caef29b0f87a2c6aaaf634b2ca4c09b6867c92", "forge-std": "https://github.com/foundry-rs/forge-std.git#f18682b2874fc57d7c80a511fed0b35ec4201ffa",
"hardhat": "^2.9.6", "hardhat": "^2.9.6",
"hardhat-deploy": "^0.11.10", "hardhat-deploy": "^0.11.10",
"hardhat-gas-reporter": "^1.0.8", "hardhat-gas-reporter": "^1.0.8",
......
import { ethers, BigNumberish, BigNumber } from 'ethers'
const iface = new ethers.utils.Interface([
'function relayMessage(address,address,bytes,uint256)',
'function relayMessage(uint256,address,address,uint256,uint256,bytes)',
])
const nonceMask = BigNumber.from(
'0x0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
)
export const big0 = BigNumber.from(0)
export const big1 = BigNumber.from(1)
/**
* Encodes the version into the nonce.
*
* @param nonce
* @param version
*/
export const encodeVersionedNonce = (
nonce: BigNumber,
version: BigNumber
): BigNumber => {
return version.or(nonce.shl(240))
}
/**
* Decodes the version from the nonce and returns the unversioned nonce as well
* as the version. The version is encoded in the first byte of
* the nonce. Note that this nonce is the nonce held in the
* CrossDomainMessenger.
*
* @param nonce
*/
export const decodeVersionedNonce = (nonce: BigNumber): BigNumber[] => {
return [nonce.and(nonceMask), nonce.shr(240)]
}
/**
* Encodes a V1 cross domain message. This message format was used before
* bedrock and does not support value transfer because ETH was represented as an
* ERC20 natively.
*
* @param target The target of the cross domain message
* @param sender The sender of the cross domain message
* @param data The data passed along with the cross domain message
* @param nonce The cross domain message nonce
*/
export const encodeCrossDomainMessageV0 = (
target: string,
sender: string,
data: string,
nonce: BigNumber
) => {
return iface.encodeFunctionData(
'relayMessage(address,address,bytes,uint256)',
[target, sender, data, nonce]
)
}
/**
* Encodes a V1 cross domain message. This message format shipped with bedrock
* and supports value transfer with native ETH.
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const encodeCrossDomainMessageV1 = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumberish,
gasLimit: BigNumberish,
data: string
) => {
return iface.encodeFunctionData(
'relayMessage(uint256,address,address,uint256,uint256,bytes)',
[nonce, sender, target, value, gasLimit, data]
)
}
/**
* Encodes a cross domain message. The version byte in the nonce determines
* the serialization format that is used.
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const encodeCrossDomainMessage = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumber,
gasLimit: BigNumber,
data: string
) => {
const [, version] = decodeVersionedNonce(nonce)
if (version.eq(big0)) {
return encodeCrossDomainMessageV0(target, sender, data, nonce)
} else if (version.eq(big1)) {
return encodeCrossDomainMessageV1(
nonce,
sender,
target,
value,
gasLimit,
data
)
}
throw new Error(`unknown version ${version.toString()}`)
}
import { BigNumberish, BigNumber, utils } from 'ethers'
const { keccak256, defaultAbiCoder } = utils
import {
decodeVersionedNonce,
encodeCrossDomainMessageV0,
encodeCrossDomainMessageV1,
big0,
big1,
} from './encoding'
export interface OutputRootProof {
version: string
stateRoot: string
withdrawerStorageRoot: string
latestBlockhash: string
}
/**
* Hahses a cross domain message.
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const hashCrossDomainMessage = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumber,
gasLimit: BigNumber,
data: string
) => {
const [, version] = decodeVersionedNonce(nonce)
if (version.eq(big0)) {
return hashCrossDomainMessagev0(target, sender, data, nonce)
} else if (version.eq(big1)) {
return hashCrossDomainMessagev1(
nonce,
sender,
target,
value,
gasLimit,
data
)
}
throw new Error(`unknown version ${version.toString()}`)
}
/**
* Hahses a V0 cross domain message
*
* @param target The target of the cross domain message
* @param sender The sender of the cross domain message
* @param data The data passed along with the cross domain message
* @param nonce The cross domain message nonce
*/
export const hashCrossDomainMessagev0 = (
target: string,
sender: string,
data: string,
nonce: BigNumber
) => {
return keccak256(encodeCrossDomainMessageV0(target, sender, data, nonce))
}
/**
* Hahses a V1 cross domain message
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const hashCrossDomainMessagev1 = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumberish,
gasLimit: BigNumberish,
data: string
) => {
return keccak256(
encodeCrossDomainMessageV1(nonce, sender, target, value, gasLimit, data)
)
}
/**
* Hashes a withdrawal
*
* @param nonce The cross domain message nonce
* @param sender The sender of the cross domain message
* @param target The target of the cross domain message
* @param value The value being sent with the cross domain message
* @param gasLimit The gas limit of the cross domain execution
* @param data The data passed along with the cross domain message
*/
export const hashWithdrawal = (
nonce: BigNumber,
sender: string,
target: string,
value: BigNumber,
gasLimit: BigNumber,
data: string
): string => {
const types = ['uint256', 'address', 'address', 'uint256', 'uint256', 'bytes']
const encoded = defaultAbiCoder.encode(types, [
nonce,
sender,
target,
value,
gasLimit,
data,
])
return keccak256(encoded)
}
/**
* Hahses an output root proof
*
* @param proof OutputRootProof
*/
export const hashOutputRootProof = (proof: OutputRootProof): string => {
return keccak256(
defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'bytes32'],
[
proof.version,
proof.stateRoot,
proof.withdrawerStorageRoot,
proof.latestBlockhash,
]
)
)
}
...@@ -8,3 +8,5 @@ export * from './fees' ...@@ -8,3 +8,5 @@ export * from './fees'
export * from './rollup-types' export * from './rollup-types'
export * from './op-node' export * from './op-node'
export * from './deposit-transaction' export * from './deposit-transaction'
export * from './encoding'
export * from './hashing'
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment