Commit b4c313da authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Support fetching preimage leaf data from transactions (#9122)

* op-challenger: Add methods retrieve leaf data from oracle contract bindings.

* op-challenger: Add leaf fetcher to extract leaf data from transactions

* op-challenger: Remove input length validations from DecodeInputData.

* op-challenger: Update fetcher to work with InputData
parent d3041fcd
......@@ -3,6 +3,7 @@ package contracts
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
......@@ -24,6 +25,12 @@ const (
methodProposalCount = "proposalCount"
methodProposals = "proposals"
methodProposalMetadata = "proposalMetadata"
methodProposalBlocksLen = "proposalBlocksLen"
methodProposalBlocks = "proposalBlocks"
)
var (
ErrInvalidAddLeavesCall = errors.New("tx is not a valid addLeaves call")
)
// PreimageOracleContract is a binding that works with contracts implementing the IPreimageOracle interface
......@@ -61,7 +68,7 @@ func (p MerkleProof) toSized() [][32]byte {
}
func NewPreimageOracleContract(addr common.Address, caller *batching.MultiCaller) (*PreimageOracleContract, error) {
mipsAbi, err := bindings.PreimageOracleMetaData.GetAbi()
oracleAbi, err := bindings.PreimageOracleMetaData.GetAbi()
if err != nil {
return nil, fmt.Errorf("failed to load preimage oracle ABI: %w", err)
}
......@@ -69,7 +76,7 @@ func NewPreimageOracleContract(addr common.Address, caller *batching.MultiCaller
return &PreimageOracleContract{
addr: addr,
multiCaller: caller,
contract: batching.NewBoundContract(mipsAbi, addr),
contract: batching.NewBoundContract(oracleAbi, addr),
}, nil
}
......@@ -167,6 +174,52 @@ func (c *PreimageOracleContract) GetProposalMetadata(ctx context.Context, block
return proposals, nil
}
func (c *PreimageOracleContract) GetInputDataBlocks(ctx context.Context, block batching.Block, ident keccakTypes.LargePreimageIdent) ([]uint64, error) {
results, err := batching.ReadArray(ctx, c.multiCaller, block,
c.contract.Call(methodProposalBlocksLen, ident.Claimant, ident.UUID),
func(i *big.Int) *batching.ContractCall {
return c.contract.Call(methodProposalBlocks, ident.Claimant, ident.UUID, i)
})
if err != nil {
return nil, fmt.Errorf("failed to load proposal blocks: %w", err)
}
blockNums := make([]uint64, 0, len(results))
for _, result := range results {
blockNums = append(blockNums, result.GetUint64(0))
}
return blockNums, nil
}
// DecodeInputData returns the UUID and [keccakTypes.InputData] being added to the preimage via a addLeavesLPP call.
// An [ErrInvalidAddLeavesCall] error is returned if the call is not a valid call to addLeavesLPP.
// Otherwise, the uuid and input data is returned. The raw data supplied is returned so long as it can be parsed.
// Specifically the length of the input data is not validated to ensure it is consistent with the number of commitments.
func (c *PreimageOracleContract) DecodeInputData(data []byte) (*big.Int, keccakTypes.InputData, error) {
method, args, err := c.contract.DecodeCall(data)
if errors.Is(err, batching.ErrUnknownMethod) {
return nil, keccakTypes.InputData{}, ErrInvalidAddLeavesCall
} else if err != nil {
return nil, keccakTypes.InputData{}, err
}
if method != methodAddLeavesLPP {
return nil, keccakTypes.InputData{}, fmt.Errorf("%w: %v", ErrInvalidAddLeavesCall, method)
}
uuid := args.GetBigInt(0)
input := args.GetBytes(1)
stateCommitments := args.GetBytes32Slice(2)
finalize := args.GetBool(3)
commitments := make([]common.Hash, 0, len(stateCommitments))
for _, c := range stateCommitments {
commitments = append(commitments, c)
}
return uuid, keccakTypes.InputData{
Input: input,
Commitments: commitments,
Finalize: finalize,
}, nil
}
func (c *PreimageOracleContract) decodePreimageIdent(result *batching.CallResult) keccakTypes.LargePreimageIdent {
return keccakTypes.LargePreimageIdent{
Claimant: result.GetAddress(0),
......
......@@ -78,13 +78,13 @@ func TestPreimageOracleContract_Squeeze(t *testing.T) {
uuid := big.NewInt(123)
stateMatrix := matrix.NewStateMatrix()
preState := keccakTypes.Leaf{
Input: [136]byte{0x12},
Input: [keccakTypes.BlockSize]byte{0x12},
Index: big.NewInt(123),
StateCommitment: common.Hash{0x34},
}
preStateProof := MerkleProof{{0x34}}
postState := keccakTypes.Leaf{
Input: [136]byte{0x34},
Input: [keccakTypes.BlockSize]byte{0x34},
Index: big.NewInt(456),
StateCommitment: common.Hash{0x56},
}
......@@ -288,3 +288,156 @@ func TestMetadata_Countered(t *testing.T) {
meta.setCountered(false)
require.False(t, meta.countered())
}
func TestGetInputDataBlocks(t *testing.T) {
stubRpc, oracle := setupPreimageOracleTest(t)
block := batching.BlockByHash(common.Hash{0xaa})
preimage := keccakTypes.LargePreimageIdent{
Claimant: common.Address{0xbb},
UUID: big.NewInt(2222),
}
stubRpc.SetResponse(
oracleAddr,
methodProposalBlocksLen,
block,
[]interface{}{preimage.Claimant, preimage.UUID},
[]interface{}{big.NewInt(3)})
blockNums := []uint64{10, 35, 67}
for i, blockNum := range blockNums {
stubRpc.SetResponse(
oracleAddr,
methodProposalBlocks,
block,
[]interface{}{preimage.Claimant, preimage.UUID, big.NewInt(int64(i))},
[]interface{}{blockNum})
}
actual, err := oracle.GetInputDataBlocks(context.Background(), block, preimage)
require.NoError(t, err)
require.Len(t, actual, 3)
require.Equal(t, blockNums, actual)
}
func TestDecodeInputData(t *testing.T) {
dataOfLength := func(len int) []byte {
data := make([]byte, len)
for i := range data {
data[i] = byte(i)
}
return data
}
ident := keccakTypes.LargePreimageIdent{
Claimant: common.Address{0xaa},
UUID: big.NewInt(1111),
}
_, oracle := setupPreimageOracleTest(t)
tests := []struct {
name string
input []byte
inputData keccakTypes.InputData
expectedTxData string
expectedErr error
}{
{
name: "UnknownMethod",
input: []byte{0xaa, 0xbb, 0xcc, 0xdd},
expectedTxData: "aabbccdd",
expectedErr: ErrInvalidAddLeavesCall,
},
{
name: "SingleInput",
inputData: keccakTypes.InputData{
Input: dataOfLength(keccakTypes.BlockSize),
Commitments: []common.Hash{{0xaa}},
Finalize: false,
},
expectedTxData: "9f99ef8200000000000000000000000000000000000000000000000000000000000004570000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000088000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818283848586870000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001aa00000000000000000000000000000000000000000000000000000000000000",
},
{
name: "MultipleInputs",
inputData: keccakTypes.InputData{
Input: dataOfLength(2 * keccakTypes.BlockSize),
Commitments: []common.Hash{{0xaa}, {0xbb}},
Finalize: false,
},
expectedTxData: "9f99ef820000000000000000000000000000000000000000000000000000000000000457000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000110000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002aa00000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000",
},
{
name: "MultipleInputs-InputTooShort",
inputData: keccakTypes.InputData{
Input: dataOfLength(2*keccakTypes.BlockSize - 10),
Commitments: []common.Hash{{0xaa}, {0xbb}},
Finalize: false,
},
expectedTxData: "9f99ef820000000000000000000000000000000000000000000000000000000000000457000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000106000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff00010203040500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002aa00000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000",
},
{
name: "MultipleInputs-FinalizeDoesNotPadInput",
inputData: keccakTypes.InputData{
Input: dataOfLength(2*keccakTypes.BlockSize - 15),
Commitments: []common.Hash{{0xaa}, {0xbb}},
Finalize: true,
},
expectedTxData: "9f99ef820000000000000000000000000000000000000000000000000000000000000457000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002aa00000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000",
},
{
name: "MultipleInputs-FinalizePadding-FullBlock",
inputData: keccakTypes.InputData{
Input: dataOfLength(2 * keccakTypes.BlockSize),
Commitments: []common.Hash{{0xaa}, {0xbb}},
Finalize: true,
},
expectedTxData: "9f99ef820000000000000000000000000000000000000000000000000000000000000457000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000110000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002aa00000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000",
},
{
name: "MultipleInputs-FinalizePadding-TrailingZeros",
inputData: keccakTypes.InputData{
Input: make([]byte, 2*keccakTypes.BlockSize),
Commitments: []common.Hash{{0xaa}, {0xbb}},
Finalize: true,
},
expectedTxData: "9f99ef820000000000000000000000000000000000000000000000000000000000000457000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002aa00000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000",
},
{
name: "MultipleInputs-FinalizePadding-ShorterThanSingleBlock",
inputData: keccakTypes.InputData{
Input: dataOfLength(keccakTypes.BlockSize - 5),
Commitments: []common.Hash{{0xaa}, {0xbb}},
Finalize: true,
},
expectedTxData: "9f99ef8200000000000000000000000000000000000000000000000000000000000004570000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000083000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f80818200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002aa00000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000",
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
var input []byte
if len(test.input) > 0 {
input = test.input
} else {
input = toAddLeavesTxData(t, oracle, ident.UUID, test.inputData)
}
require.Equal(t, test.expectedTxData, common.Bytes2Hex(input),
"ABI has been changed. Add tests to ensure historic transactions can be parsed before updating expectedTxData")
uuid, leaves, err := oracle.DecodeInputData(input)
if test.expectedErr != nil {
require.ErrorIs(t, err, test.expectedErr)
} else {
require.NoError(t, err)
require.Equal(t, ident.UUID, uuid)
require.Equal(t, test.inputData, leaves)
}
})
}
}
func toAddLeavesTxData(t *testing.T, oracle *PreimageOracleContract, uuid *big.Int, inputData keccakTypes.InputData) []byte {
tx, err := oracle.AddLeaves(uuid, inputData.Input, inputData.Commitments, inputData.Finalize)
require.NoError(t, err)
return tx.TxData
}
package fetcher
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
var (
ErrNoLeavesFound = errors.New("no leaves found in block")
)
type L1Source interface {
TxsByNumber(ctx context.Context, number uint64) (types.Transactions, error)
FetchReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
ChainID(ctx context.Context) (*big.Int, error)
}
type Oracle interface {
Addr() common.Address
GetInputDataBlocks(ctx context.Context, block batching.Block, ident keccakTypes.LargePreimageIdent) ([]uint64, error)
DecodeInputData(data []byte) (*big.Int, keccakTypes.InputData, error)
}
type InputFetcher struct {
log log.Logger
source L1Source
}
func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, oracle Oracle, ident keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error) {
blockNums, err := oracle.GetInputDataBlocks(ctx, batching.BlockByHash(blockHash), ident)
if err != nil {
return nil, fmt.Errorf("failed to retrieve leaf block nums: %w", err)
}
chainID, err := f.source.ChainID(ctx)
if err != nil {
return nil, fmt.Errorf("failed to retrieve L1 chain ID: %w", err)
}
signer := types.LatestSignerForChainID(chainID)
var inputs []keccakTypes.InputData
for _, blockNum := range blockNums {
foundRelevantTx := false
txs, err := f.source.TxsByNumber(ctx, blockNum)
if err != nil {
return nil, fmt.Errorf("failed getting tx for block %v: %w", blockNum, err)
}
for _, tx := range txs {
inputData, err := f.extractRelevantLeavesFromTx(ctx, oracle, signer, tx, ident)
if err != nil {
return nil, err
}
if inputData != nil {
foundRelevantTx = true
inputs = append(inputs, *inputData)
}
}
if !foundRelevantTx {
// The contract said there was a relevant transaction in this block that we failed to find.
// There was either a reorg or the extraction logic is broken.
// Either way, abort this attempt to validate the preimage.
return nil, fmt.Errorf("%w %v", ErrNoLeavesFound, blockNum)
}
}
return inputs, nil
}
func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle Oracle, signer types.Signer, tx *types.Transaction, ident keccakTypes.LargePreimageIdent) (*keccakTypes.InputData, error) {
if tx.To() == nil || *tx.To() != oracle.Addr() {
f.log.Trace("Skip tx with incorrect to addr", "tx", tx.Hash(), "expected", oracle.Addr(), "actual", tx.To())
return nil, nil
}
uuid, inputData, err := oracle.DecodeInputData(tx.Data())
if errors.Is(err, contracts.ErrInvalidAddLeavesCall) {
f.log.Trace("Skip tx with invalid call data", "tx", tx.Hash(), "err", err)
return nil, nil
} else if err != nil {
return nil, err
}
if uuid.Cmp(ident.UUID) != 0 {
f.log.Trace("Skip tx with incorrect UUID", "tx", tx.Hash(), "expected", ident.UUID, "actual", uuid)
return nil, nil
}
sender, err := signer.Sender(tx)
if err != nil {
f.log.Trace("Skipping transaction with invalid sender", "tx", tx.Hash(), "err", err)
return nil, nil
}
if sender != ident.Claimant {
f.log.Trace("Skipping transaction with incorrect sender", "tx", tx.Hash(), "expected", ident.Claimant, "actual", sender)
return nil, nil
}
rcpt, err := f.source.FetchReceipt(ctx, tx.Hash())
if err != nil {
return nil, fmt.Errorf("failed to retrieve receipt for tx %v: %w", tx.Hash(), err)
}
if rcpt.Status != types.ReceiptStatusSuccessful {
f.log.Trace("Skipping transaction with failed receipt status", "tx", tx.Hash(), "status", rcpt.Status)
return nil, nil
}
return &inputData, nil
}
func NewPreimageFetcher(logger log.Logger, source L1Source) *InputFetcher {
return &InputFetcher{
log: logger,
source: source,
}
}
package fetcher
import (
"context"
"crypto/ecdsa"
"errors"
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum-optimism/optimism/op-service/testlog"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
"github.com/stretchr/testify/require"
)
var (
oracleAddr = common.Address{0x99, 0x98}
privKey, _ = crypto.GenerateKey()
ident = keccakTypes.LargePreimageIdent{
Claimant: crypto.PubkeyToAddress(privKey.PublicKey),
UUID: big.NewInt(888),
}
chainID = big.NewInt(123)
blockHash = common.Hash{0xdd}
input1 = keccakTypes.InputData{
Input: []byte{0xbb, 0x11},
Commitments: []common.Hash{{0xcc, 0x11}},
}
input2 = keccakTypes.InputData{
Input: []byte{0xbb, 0x22},
Commitments: []common.Hash{{0xcc, 0x22}},
}
input3 = keccakTypes.InputData{
Input: []byte{0xbb, 0x33},
Commitments: []common.Hash{{0xcc, 0x33}},
}
input4 = keccakTypes.InputData{
Input: []byte{0xbb, 0x44},
Commitments: []common.Hash{{0xcc, 0x44}},
Finalize: true,
}
)
func TestFetchLeaves_NoBlocks(t *testing.T) {
fetcher, oracle, _ := setupFetcherTest(t)
oracle.leafBlocks = []uint64{}
leaves, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Empty(t, leaves)
}
func TestFetchLeaves_SingleTx(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
l1Source.txs[blockNum] = types.Transactions{oracle.txForInput(ValidTx, input1)}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_MultipleBlocksAndLeaves(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
block1 := uint64(7)
block2 := uint64(15)
block3 := uint64(20)
oracle.leafBlocks = []uint64{block1, block2, block3}
l1Source.txs[block1] = types.Transactions{oracle.txForInput(ValidTx, input1)}
l1Source.txs[block2] = types.Transactions{oracle.txForInput(ValidTx, input2)}
l1Source.txs[block3] = types.Transactions{oracle.txForInput(ValidTx, input3), oracle.txForInput(ValidTx, input4)}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1, input2, input3, input4}, inputs)
}
func TestFetchLeaves_SkipTxToWrongContract(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Valid tx but to a different contract
tx1 := oracle.txForInput(WithToAddr(common.Address{0x88, 0x99, 0x11}), input2)
// Valid tx but without a to addr
tx2 := oracle.txForInput(WithoutToAddr(), input2)
// Valid tx to the correct contract
tx3 := oracle.txForInput(ValidTx, input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2, tx3}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_SkipTxWithDifferentUUID(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Valid tx but with a different UUID
tx1 := oracle.txForInput(WithUUID(big.NewInt(874927294)), input2)
// Valid tx
tx2 := oracle.txForInput(ValidTx, input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_SkipTxWithInvalidCall(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Call to preimage oracle but fails to decode
tx1 := oracle.txForInput(WithInvalidData(), input2)
// Valid tx
tx2 := oracle.txForInput(ValidTx, input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_SkipTxWithInvalidSender(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Call to preimage oracle with different Chain ID
tx1 := oracle.txForInput(WithChainID(big.NewInt(992)), input3)
// Call to preimage oracle with wrong sender
wrongKey, _ := crypto.GenerateKey()
tx2 := oracle.txForInput(WithPrivKey(wrongKey), input4)
// Valid tx
tx3 := oracle.txForInput(ValidTx, input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2, tx3}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_SkipTxWithReceiptStatusFail(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Valid call to the preimage oracle but that reverted
tx1 := oracle.txForInput(ValidTx, input2)
l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed
// Valid tx
tx2 := oracle.txForInput(ValidTx, input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2}
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_ErrorsWhenNoValidLeavesInBlock(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Irrelevant call
tx1 := oracle.txForInput(WithUUID(big.NewInt(492)), input2)
l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed
l1Source.txs[blockNum] = types.Transactions{tx1}
_, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.ErrorIs(t, err, ErrNoLeavesFound)
}
func setupFetcherTest(t *testing.T) (*InputFetcher, *stubOracle, *stubL1Source) {
oracle := &stubOracle{
txInputs: make(map[byte]keccakTypes.InputData),
}
l1Source := &stubL1Source{
txs: make(map[uint64]types.Transactions),
rcptStatus: make(map[common.Hash]uint64),
}
fetcher := NewPreimageFetcher(testlog.Logger(t, log.LvlTrace), l1Source)
return fetcher, oracle, l1Source
}
type stubOracle struct {
nextTxId byte
leafBlocks []uint64
txInputs map[byte]keccakTypes.InputData
}
func (o *stubOracle) Addr() common.Address {
return oracleAddr
}
func (o *stubOracle) GetInputDataBlocks(_ context.Context, _ batching.Block, _ keccakTypes.LargePreimageIdent) ([]uint64, error) {
return o.leafBlocks, nil
}
func (o *stubOracle) DecodeInputData(data []byte) (*big.Int, keccakTypes.InputData, error) {
if len(data) == 0 {
return nil, keccakTypes.InputData{}, contracts.ErrInvalidAddLeavesCall
}
input, ok := o.txInputs[data[0]]
if !ok {
return nil, keccakTypes.InputData{}, contracts.ErrInvalidAddLeavesCall
}
uuid := ident.UUID
// WithUUID appends custom UUIDs to the tx data
if len(data) > 1 {
uuid = new(big.Int).SetBytes(data[1:])
}
return uuid, input, nil
}
type TxModifier func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey
var ValidTx TxModifier = func(_ *types.DynamicFeeTx) *ecdsa.PrivateKey {
return privKey
}
func WithToAddr(addr common.Address) TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey {
tx.To = &addr
return privKey
}
}
func WithoutToAddr() TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey {
tx.To = nil
return privKey
}
}
func WithUUID(uuid *big.Int) TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey {
tx.Data = append(tx.Data, uuid.Bytes()...)
return privKey
}
}
func WithInvalidData() TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey {
tx.Data = []byte{}
return privKey
}
}
func WithChainID(id *big.Int) TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey {
tx.ChainID = id
return privKey
}
}
func WithPrivKey(key *ecdsa.PrivateKey) TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey {
return key
}
}
func (o *stubOracle) txForInput(txMod TxModifier, input keccakTypes.InputData) *types.Transaction {
id := o.nextTxId
o.nextTxId++
o.txInputs[id] = input
inner := &types.DynamicFeeTx{
ChainID: chainID,
Nonce: 1,
To: &oracleAddr,
Value: big.NewInt(0),
GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(2),
Gas: 3,
Data: []byte{id},
}
key := txMod(inner)
tx := types.MustSignNewTx(key, types.LatestSignerForChainID(inner.ChainID), inner)
return tx
}
type stubL1Source struct {
txs map[uint64]types.Transactions
rcptStatus map[common.Hash]uint64
}
func (s *stubL1Source) ChainID(_ context.Context) (*big.Int, error) {
return chainID, nil
}
func (s *stubL1Source) TxsByNumber(_ context.Context, number uint64) (types.Transactions, error) {
txs, ok := s.txs[number]
if !ok {
return nil, errors.New("not found")
}
return txs, nil
}
func (s *stubL1Source) FetchReceipt(_ context.Context, txHash common.Hash) (*types.Receipt, error) {
rcptStatus, ok := s.rcptStatus[txHash]
if !ok {
rcptStatus = types.ReceiptStatusSuccessful
}
return &types.Receipt{Status: rcptStatus}, nil
}
package batching
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
)
var (
ErrUnknownMethod = errors.New("unknown method")
ErrInvalidCall = errors.New("invalid call")
)
type BoundContract struct {
abi *abi.ABI
addr common.Address
}
func NewBoundContract(abi *abi.ABI, addr common.Address) *BoundContract {
return &BoundContract{
abi: abi,
addr: addr,
}
}
func (b *BoundContract) Call(method string, args ...interface{}) *ContractCall {
return NewContractCall(b.abi, b.addr, method, args...)
}
func (b *BoundContract) DecodeCall(data []byte) (string, *CallResult, error) {
if len(data) < 4 {
return "", nil, ErrUnknownMethod
}
method, err := b.abi.MethodById(data[:4])
if err != nil {
// ABI doesn't return a nicely typed error so treat any failure to find the method as unknown
return "", nil, fmt.Errorf("%w: %v", ErrUnknownMethod, err.Error())
}
args, err := method.Inputs.Unpack(data[4:])
if err != nil {
return "", nil, fmt.Errorf("%w: %v", ErrInvalidCall, err.Error())
}
return method.Name, &CallResult{args}, nil
}
package batching
import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestDecodeCall(t *testing.T) {
method := "approve"
spender := common.Address{0xbb, 0xee}
amount := big.NewInt(4242)
testAbi, err := bindings.ERC20MetaData.GetAbi()
require.NoError(t, err)
validData, err := testAbi.Pack(method, spender, amount)
require.NoError(t, err)
contract := NewBoundContract(testAbi, common.Address{0xaa})
t.Run("TooShort", func(t *testing.T) {
_, _, err := contract.DecodeCall([]byte{1, 2, 3})
require.ErrorIs(t, err, ErrUnknownMethod)
})
t.Run("UnknownMethodId", func(t *testing.T) {
_, _, err := contract.DecodeCall([]byte{1, 2, 3, 4})
require.ErrorIs(t, err, ErrUnknownMethod)
})
t.Run("MissingArgs", func(t *testing.T) {
// Truncate to just the 4 byte method selector
_, _, err = contract.DecodeCall(validData[:4])
require.ErrorIs(t, err, ErrInvalidCall)
// Truncate to partial args
_, _, err = contract.DecodeCall(validData[:6])
require.ErrorIs(t, err, ErrInvalidCall)
// Truncate to first arg but missing second
_, _, err = contract.DecodeCall(validData[:24])
require.ErrorIs(t, err, ErrInvalidCall)
})
t.Run("ValidCall", func(t *testing.T) {
name, args, err := contract.DecodeCall(validData)
require.NoError(t, err)
require.Equal(t, name, method)
require.Equal(t, spender, args.GetAddress(0))
require.Zero(t, amount.Cmp(args.GetBigInt(1)))
})
}
......@@ -11,22 +11,6 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
)
type BoundContract struct {
abi *abi.ABI
addr common.Address
}
func NewBoundContract(abi *abi.ABI, addr common.Address) *BoundContract {
return &BoundContract{
abi: abi,
addr: addr,
}
}
func (b *BoundContract) Call(method string, args ...interface{}) *ContractCall {
return NewContractCall(b.abi, b.addr, method, args...)
}
type ContractCall struct {
Abi *abi.ABI
Addr common.Address
......@@ -134,6 +118,14 @@ func (c *CallResult) GetStruct(i int, target interface{}) {
abi.ConvertType(c.out[i], target)
}
func (c *CallResult) GetBytes(i int) []byte {
return *abi.ConvertType(c.out[i], new([]byte)).(*[]byte)
}
func (c *CallResult) GetBytes32(i int) [32]byte {
return *abi.ConvertType(c.out[i], new([32]byte)).(*[32]byte)
}
func (c *CallResult) GetBytes32Slice(i int) [][32]byte {
return *abi.ConvertType(c.out[i], new([][32]byte)).(*[][32]byte)
}
......@@ -148,6 +148,13 @@ func TestCallResult_GetValues(t *testing.T) {
},
expected: ([32]byte)(common.Hash{0xaa, 0xbb, 0xcc}),
},
{
name: "GetBytes",
getter: func(result *CallResult, i int) interface{} {
return result.GetBytes(i)
},
expected: []byte{0xaa, 0xbb, 0xcc},
},
{
name: "GetBytes32",
getter: func(result *CallResult, i int) interface{} {
......@@ -155,6 +162,13 @@ func TestCallResult_GetValues(t *testing.T) {
},
expected: [32]byte{0xaa, 0xbb, 0xcc},
},
{
name: "GetBytes32Slice",
getter: func(result *CallResult, i int) interface{} {
return result.GetBytes32Slice(i)
},
expected: [][32]byte{{0xaa, 0xbb, 0xcc}, {0xdd, 0xee, 0xff}, {0x11, 0x22, 0x33}},
},
{
name: "GetBigInt",
getter: func(result *CallResult, i int) interface{} {
......
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