Skip to content

Commit

Permalink
op-challenger: Support fetching preimage leaf data from transactions (e…
Browse files Browse the repository at this point in the history
…thereum-optimism#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
  • Loading branch information
ajsutton authored Jan 23, 2024
1 parent d3041fc commit b4c313d
Show file tree
Hide file tree
Showing 8 changed files with 747 additions and 20 deletions.
57 changes: 55 additions & 2 deletions op-challenger/game/fault/contracts/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package contracts
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"math/big"
Expand All @@ -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
Expand Down Expand Up @@ -61,15 +68,15 @@ 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)
}

return &PreimageOracleContract{
addr: addr,
multiCaller: caller,
contract: batching.NewBoundContract(mipsAbi, addr),
contract: batching.NewBoundContract(oracleAbi, addr),
}, nil
}

Expand Down Expand Up @@ -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),
Expand Down
157 changes: 155 additions & 2 deletions op-challenger/game/fault/contracts/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
}
Expand Down Expand Up @@ -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
}
116 changes: 116 additions & 0 deletions op-challenger/game/keccak/fetcher/fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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,
}
}
Loading

0 comments on commit b4c313d

Please sign in to comment.