Commit 1cfd38b1 authored by mbaxter's avatar mbaxter Committed by GitHub

Challenger: Pull large preimage proposals from tx logs (#10416)

* challenger: Pull large preimages from tx logs

* challenger: Add a few more fetcher unit tests

* challenger: Implement review fixes related to slice handling, types

* challenger: Clean up fetcher changes
parent 853842d1
...@@ -40,11 +40,6 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o ...@@ -40,11 +40,6 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve leaf block nums: %w", err) 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 var inputs []keccakTypes.InputData
for _, blockNum := range blockNums { for _, blockNum := range blockNums {
foundRelevantTx := false foundRelevantTx := false
...@@ -53,13 +48,13 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o ...@@ -53,13 +48,13 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o
return nil, fmt.Errorf("failed getting tx for block %v: %w", blockNum, err) return nil, fmt.Errorf("failed getting tx for block %v: %w", blockNum, err)
} }
for _, tx := range block.Transactions() { for _, tx := range block.Transactions() {
inputData, err := f.extractRelevantLeavesFromTx(ctx, oracle, signer, tx, ident) inputData, err := f.extractRelevantLeavesFromTx(ctx, oracle, tx, ident)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if inputData != nil { if len(inputData) > 0 {
foundRelevantTx = true foundRelevantTx = true
inputs = append(inputs, *inputData) inputs = append(inputs, inputData...)
} }
} }
if !foundRelevantTx { if !foundRelevantTx {
...@@ -72,31 +67,7 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o ...@@ -72,31 +67,7 @@ func (f *InputFetcher) FetchInputs(ctx context.Context, blockHash common.Hash, o
return inputs, nil return inputs, nil
} }
func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle Oracle, signer types.Signer, tx *types.Transaction, ident keccakTypes.LargePreimageIdent) (*keccakTypes.InputData, error) { func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle Oracle, 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.TransactionReceipt(ctx, tx.Hash()) rcpt, err := f.source.TransactionReceipt(ctx, tx.Hash())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to retrieve receipt for tx %v: %w", tx.Hash(), err) return nil, fmt.Errorf("failed to retrieve receipt for tx %v: %w", tx.Hash(), err)
...@@ -105,7 +76,40 @@ func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle O ...@@ -105,7 +76,40 @@ func (f *InputFetcher) extractRelevantLeavesFromTx(ctx context.Context, oracle O
f.log.Trace("Skipping transaction with failed receipt status", "tx", tx.Hash(), "status", rcpt.Status) f.log.Trace("Skipping transaction with failed receipt status", "tx", tx.Hash(), "status", rcpt.Status)
return nil, nil return nil, nil
} }
return &inputData, nil
// Iterate over the logs from in this receipt, looking for relevant logs emitted from the oracle contract
var inputs []keccakTypes.InputData
for i, txLog := range rcpt.Logs {
if txLog.Address != oracle.Addr() {
f.log.Trace("Skip tx log not emitted by the oracle contract", "tx", tx.Hash(), "logIndex", i, "targetContract", oracle.Addr(), "actualContract", txLog.Address)
continue
}
if len(txLog.Data) < 20 {
f.log.Trace("Skip tx log with insufficient data (less than 20 bytes)", "tx", tx.Hash(), "logIndex", i, "dataLength", len(txLog.Data))
continue
}
caller := common.Address(txLog.Data[0:20])
callData := txLog.Data[20:]
if caller != ident.Claimant {
f.log.Trace("Skip tx log from irrelevant claimant", "tx", tx.Hash(), "logIndex", i, "targetClaimant", ident.Claimant, "actualClaimant", caller)
continue
}
uuid, inputData, err := oracle.DecodeInputData(callData)
if errors.Is(err, contracts.ErrInvalidAddLeavesCall) {
f.log.Trace("Skip tx log with call data not targeting expected method", "tx", tx.Hash(), "logIndex", i, "err", err)
continue
} else if err != nil {
return nil, err
}
if uuid.Cmp(ident.UUID) != 0 {
f.log.Trace("Skip tx log with irrelevant UUID", "tx", tx.Hash(), "logIndex", i, "targetUUID", ident.UUID, "actualUUID", uuid)
continue
}
inputs = append(inputs, inputData)
}
return inputs, nil
} }
func NewPreimageFetcher(logger log.Logger, source L1Source) *InputFetcher { func NewPreimageFetcher(logger log.Logger, source L1Source) *InputFetcher {
......
...@@ -4,6 +4,8 @@ import ( ...@@ -4,6 +4,8 @@ import (
"context" "context"
"crypto/ecdsa" "crypto/ecdsa"
"errors" "errors"
"fmt"
"math"
"math/big" "math/big"
"testing" "testing"
...@@ -18,11 +20,18 @@ import ( ...@@ -18,11 +20,18 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const (
// Signal to indicate a receipt should be considered missing
MissingReceiptStatus = math.MaxUint64
)
var ( var (
oracleAddr = common.Address{0x99, 0x98} oracleAddr = common.Address{0x99, 0x98}
privKey, _ = crypto.GenerateKey() otherAddr = common.Address{0x12, 0x34}
ident = keccakTypes.LargePreimageIdent{ claimantKey, _ = crypto.GenerateKey()
Claimant: crypto.PubkeyToAddress(privKey.PublicKey), otherKey, _ = crypto.GenerateKey()
ident = keccakTypes.LargePreimageIdent{
Claimant: crypto.PubkeyToAddress(claimantKey.PublicKey),
UUID: big.NewInt(888), UUID: big.NewInt(888),
} }
chainID = big.NewInt(123) chainID = big.NewInt(123)
...@@ -54,86 +63,211 @@ func TestFetchLeaves_NoBlocks(t *testing.T) { ...@@ -54,86 +63,211 @@ func TestFetchLeaves_NoBlocks(t *testing.T) {
require.Empty(t, leaves) require.Empty(t, leaves)
} }
func TestFetchLeaves_SingleTx(t *testing.T) { func TestFetchLeaves_ErrorOnUnavailableInputBlocks(t *testing.T) {
fetcher, oracle, _ := setupFetcherTest(t)
mockErr := fmt.Errorf("oops")
oracle.inputDataBlocksError = mockErr
leaves, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.ErrorContains(t, err, "failed to retrieve leaf block nums")
require.Empty(t, leaves)
}
func TestFetchLeaves_ErrorOnUnavailableL1Block(t *testing.T) {
blockNum := uint64(7)
fetcher, oracle, _ := setupFetcherTest(t)
oracle.leafBlocks = []uint64{blockNum}
// No txs means stubL1Source will return an error when we try to fetch the block
leaves, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.ErrorContains(t, err, fmt.Sprintf("failed getting tx for block %v", blockNum))
require.Empty(t, leaves)
}
func TestFetchLeaves_SingleTxSingleLog(t *testing.T) {
cases := []struct {
name string
txSender *ecdsa.PrivateKey
txModifier TxModifier
}{
{"from EOA claimant address", claimantKey, ValidTx},
{"from contract call", otherKey, WithToAddr(otherAddr)},
{"from contract creation", otherKey, WithoutToAddr()},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
proposal := oracle.createProposal(input1)
tx := l1Source.createTx(blockNum, tc.txSender, tc.txModifier)
l1Source.createLog(tx, proposal)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
})
}
}
func TestFetchLeaves_SingleTxMultipleLogs(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
l1Source.txs[blockNum] = types.Transactions{oracle.txForInput(ValidTx, input1)}
proposal1 := oracle.createProposal(input1)
proposal2 := oracle.createProposal(input2)
tx := l1Source.createTx(blockNum, otherKey, WithToAddr(otherAddr))
l1Source.createLog(tx, proposal1)
l1Source.createLog(tx, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs) require.Equal(t, []keccakTypes.InputData{input1, input2}, inputs)
} }
func TestFetchLeaves_MultipleBlocksAndLeaves(t *testing.T) { func TestFetchLeaves_MultipleBlocksAndLeaves(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
block1 := uint64(7) block1 := uint64(7)
block2 := uint64(15) block2 := uint64(15)
block3 := uint64(20) oracle.leafBlocks = []uint64{block1, block2}
oracle.leafBlocks = []uint64{block1, block2, block3}
l1Source.txs[block1] = types.Transactions{oracle.txForInput(ValidTx, input1)} proposal1 := oracle.createProposal(input1)
l1Source.txs[block2] = types.Transactions{oracle.txForInput(ValidTx, input2)} proposal2 := oracle.createProposal(input2)
l1Source.txs[block3] = types.Transactions{oracle.txForInput(ValidTx, input3), oracle.txForInput(ValidTx, input4)} proposal3 := oracle.createProposal(input3)
proposal4 := oracle.createProposal(input4)
block1Tx := l1Source.createTx(block1, claimantKey, ValidTx)
block2TxA := l1Source.createTx(block2, claimantKey, ValidTx)
l1Source.createTx(block2, claimantKey, ValidTx) // Add tx with no logs
block2TxB := l1Source.createTx(block2, otherKey, WithoutToAddr())
l1Source.createLog(block1Tx, proposal1)
l1Source.createLog(block2TxA, proposal2)
l1Source.createLog(block2TxB, proposal3)
l1Source.createLog(block2TxB, proposal4)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1, input2, input3, input4}, inputs) require.Equal(t, []keccakTypes.InputData{input1, input2, input3, input4}, inputs)
} }
func TestFetchLeaves_SkipTxToWrongContract(t *testing.T) { func TestFetchLeaves_SkipLogFromWrongContract(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
// Valid tx but to a different contract
tx1 := oracle.txForInput(WithToAddr(common.Address{0x88, 0x99, 0x11}), input2) // Emit log from an irrelevant contract address
// Valid tx but without a to addr proposal1 := oracle.createProposal(input2)
tx2 := oracle.txForInput(WithoutToAddr(), input2) tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
// Valid tx to the correct contract log1 := l1Source.createLog(tx1, proposal1)
tx3 := oracle.txForInput(ValidTx, input1) log1.Address = otherAddr
l1Source.txs[blockNum] = types.Transactions{tx1, tx2, tx3} // Valid tx
proposal2 := oracle.createProposal(input1)
tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs) require.Equal(t, []keccakTypes.InputData{input1}, inputs)
} }
func TestFetchLeaves_SkipTxWithDifferentUUID(t *testing.T) { func TestFetchLeaves_SkipProposalWithWrongUUID(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
// Valid tx but with a different UUID // Valid tx but with a different UUID
tx1 := oracle.txForInput(WithUUID(big.NewInt(874927294)), input2) proposal1 := oracle.createProposal(input2)
proposal1.uuid = big.NewInt(874927294)
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx1, proposal1)
// Valid tx
proposal2 := oracle.createProposal(input1)
tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_SkipProposalWithWrongClaimant(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Valid tx but with a different claimant
proposal1 := oracle.createProposal(input2)
proposal1.claimantAddr = otherAddr
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx1, proposal1)
// Valid tx
proposal2 := oracle.createProposal(input1)
tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs)
}
func TestFetchLeaves_SkipInvalidProposal(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Set up proposal decoding to fail
proposal1 := oracle.createProposal(input2)
proposal1.valid = false
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx1, proposal1)
// Valid tx // Valid tx
tx2 := oracle.txForInput(ValidTx, input1) proposal2 := oracle.createProposal(input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2} tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs) require.Equal(t, []keccakTypes.InputData{input1}, inputs)
} }
func TestFetchLeaves_SkipTxWithInvalidCall(t *testing.T) { func TestFetchLeaves_SkipProposalWithInsufficientData(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
// Call to preimage oracle but fails to decode
tx1 := oracle.txForInput(WithInvalidData(), input2) // Log contains insufficient data
// It should hold a 20 byte address followed by the proposal payload
proposal1 := oracle.createProposal(input2)
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
log1 := l1Source.createLog(tx1, proposal1)
log1.Data = proposal1.claimantAddr[:19]
// Valid tx // Valid tx
tx2 := oracle.txForInput(ValidTx, input1) proposal2 := oracle.createProposal(input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2} tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs) require.Equal(t, []keccakTypes.InputData{input1}, inputs)
} }
func TestFetchLeaves_SkipTxWithInvalidSender(t *testing.T) { func TestFetchLeaves_SkipProposalMissingCallData(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
// Call to preimage oracle with different Chain ID
tx1 := oracle.txForInput(WithChainID(big.NewInt(992)), input3) // Truncate call data from log so that is only contains an address
// Call to preimage oracle with wrong sender proposal1 := oracle.createProposal(input2)
wrongKey, _ := crypto.GenerateKey() tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
tx2 := oracle.txForInput(WithPrivKey(wrongKey), input4) log1 := l1Source.createLog(tx1, proposal1)
log1.Data = log1.Data[0:20]
// Valid tx // Valid tx
tx3 := oracle.txForInput(ValidTx, input1) proposal2 := oracle.createProposal(input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2, tx3} tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs) require.Equal(t, []keccakTypes.InputData{input1}, inputs)
...@@ -143,45 +277,87 @@ func TestFetchLeaves_SkipTxWithReceiptStatusFail(t *testing.T) { ...@@ -143,45 +277,87 @@ func TestFetchLeaves_SkipTxWithReceiptStatusFail(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
// Valid call to the preimage oracle but that reverted
tx1 := oracle.txForInput(ValidTx, input2) // Valid proposal, but tx reverted
proposal1 := oracle.createProposal(input2)
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx1, proposal1)
l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed
// Valid tx // Valid tx
tx2 := oracle.txForInput(ValidTx, input1) proposal2 := oracle.createProposal(input1)
l1Source.txs[blockNum] = types.Transactions{tx1, tx2} tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []keccakTypes.InputData{input1}, inputs) require.Equal(t, []keccakTypes.InputData{input1}, inputs)
} }
func TestFetchLeaves_ErrorsOnMissingReceipt(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum}
// Valid tx
proposal1 := oracle.createProposal(input1)
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx1, proposal1)
// Valid proposal, but tx receipt is missing
proposal2 := oracle.createProposal(input2)
tx2 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx2, proposal2)
l1Source.rcptStatus[tx2.Hash()] = MissingReceiptStatus
input, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.ErrorContains(t, err, fmt.Sprintf("failed to retrieve receipt for tx %v", tx2.Hash()))
require.Nil(t, input)
}
func TestFetchLeaves_ErrorsWhenNoValidLeavesInBlock(t *testing.T) { func TestFetchLeaves_ErrorsWhenNoValidLeavesInBlock(t *testing.T) {
fetcher, oracle, l1Source := setupFetcherTest(t) fetcher, oracle, l1Source := setupFetcherTest(t)
blockNum := uint64(7) blockNum := uint64(7)
oracle.leafBlocks = []uint64{blockNum} oracle.leafBlocks = []uint64{blockNum}
// Irrelevant call
tx1 := oracle.txForInput(WithUUID(big.NewInt(492)), input2) // Irrelevant tx - reverted
proposal1 := oracle.createProposal(input2)
tx1 := l1Source.createTx(blockNum, claimantKey, ValidTx)
l1Source.createLog(tx1, proposal1)
l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed l1Source.rcptStatus[tx1.Hash()] = types.ReceiptStatusFailed
l1Source.txs[blockNum] = types.Transactions{tx1} // Irrelevant tx - no logs are emitted
_, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident) l1Source.createTx(blockNum, claimantKey, ValidTx)
inputs, err := fetcher.FetchInputs(context.Background(), blockHash, oracle, ident)
require.ErrorIs(t, err, ErrNoLeavesFound) require.ErrorIs(t, err, ErrNoLeavesFound)
require.Nil(t, inputs)
} }
func setupFetcherTest(t *testing.T) (*InputFetcher, *stubOracle, *stubL1Source) { func setupFetcherTest(t *testing.T) (*InputFetcher, *stubOracle, *stubL1Source) {
oracle := &stubOracle{ oracle := &stubOracle{
txInputs: make(map[byte]keccakTypes.InputData), proposals: make(map[byte]*proposalConfig),
} }
l1Source := &stubL1Source{ l1Source := &stubL1Source{
txs: make(map[uint64]types.Transactions), txs: make(map[uint64]types.Transactions),
rcptStatus: make(map[common.Hash]uint64), rcptStatus: make(map[common.Hash]uint64),
logs: make(map[common.Hash][]*types.Log),
} }
fetcher := NewPreimageFetcher(testlog.Logger(t, log.LevelTrace), l1Source) fetcher := NewPreimageFetcher(testlog.Logger(t, log.LevelTrace), l1Source)
return fetcher, oracle, l1Source return fetcher, oracle, l1Source
} }
type proposalConfig struct {
id byte
claimantAddr common.Address
inputData keccakTypes.InputData
uuid *big.Int
valid bool
}
type stubOracle struct { type stubOracle struct {
nextTxId byte leafBlocks []uint64
leafBlocks []uint64 nextProposalId byte
txInputs map[byte]keccakTypes.InputData proposals map[byte]*proposalConfig
// Add a field to allow for mocking of errors
inputDataBlocksError error
} }
func (o *stubOracle) Addr() common.Address { func (o *stubOracle) Addr() common.Address {
...@@ -189,6 +365,9 @@ func (o *stubOracle) Addr() common.Address { ...@@ -189,6 +365,9 @@ func (o *stubOracle) Addr() common.Address {
} }
func (o *stubOracle) GetInputDataBlocks(_ context.Context, _ rpcblock.Block, _ keccakTypes.LargePreimageIdent) ([]uint64, error) { func (o *stubOracle) GetInputDataBlocks(_ context.Context, _ rpcblock.Block, _ keccakTypes.LargePreimageIdent) ([]uint64, error) {
if o.inputDataBlocksError != nil {
return nil, o.inputDataBlocksError
}
return o.leafBlocks, nil return o.leafBlocks, nil
} }
...@@ -196,105 +375,131 @@ func (o *stubOracle) DecodeInputData(data []byte) (*big.Int, keccakTypes.InputDa ...@@ -196,105 +375,131 @@ func (o *stubOracle) DecodeInputData(data []byte) (*big.Int, keccakTypes.InputDa
if len(data) == 0 { if len(data) == 0 {
return nil, keccakTypes.InputData{}, contracts.ErrInvalidAddLeavesCall return nil, keccakTypes.InputData{}, contracts.ErrInvalidAddLeavesCall
} }
input, ok := o.txInputs[data[0]] proposalId := data[0]
if !ok { proposal, ok := o.proposals[proposalId]
if !ok || !proposal.valid {
return nil, keccakTypes.InputData{}, contracts.ErrInvalidAddLeavesCall return nil, keccakTypes.InputData{}, contracts.ErrInvalidAddLeavesCall
} }
uuid := ident.UUID
// WithUUID appends custom UUIDs to the tx data return proposal.uuid, proposal.inputData, nil
if len(data) > 1 {
uuid = new(big.Int).SetBytes(data[1:])
}
return uuid, input, nil
} }
type TxModifier func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey type TxModifier func(tx *types.DynamicFeeTx)
var ValidTx TxModifier = func(_ *types.DynamicFeeTx) *ecdsa.PrivateKey { var ValidTx TxModifier = func(_ *types.DynamicFeeTx) {
return privKey // no-op
} }
func WithToAddr(addr common.Address) TxModifier { func WithToAddr(addr common.Address) TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey { return func(tx *types.DynamicFeeTx) {
tx.To = &addr tx.To = &addr
return privKey
} }
} }
func WithoutToAddr() TxModifier { func WithoutToAddr() TxModifier {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey { return func(tx *types.DynamicFeeTx) {
tx.To = nil tx.To = nil
return privKey
} }
} }
func WithUUID(uuid *big.Int) TxModifier { func (o *stubOracle) createProposal(input keccakTypes.InputData) *proposalConfig {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey { id := o.nextProposalId
tx.Data = append(tx.Data, uuid.Bytes()...) o.nextProposalId++
return privKey
proposal := &proposalConfig{
id: id,
claimantAddr: ident.Claimant,
inputData: input,
uuid: ident.UUID,
valid: true,
} }
o.proposals[id] = proposal
return proposal
} }
func WithInvalidData() TxModifier { type stubL1Source struct {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey { nextTxId uint64
tx.Data = []byte{} // Map block number to tx
return privKey txs map[uint64]types.Transactions
} // Map txHash to receipt
rcptStatus map[common.Hash]uint64
// Map txHash to logs
logs map[common.Hash][]*types.Log
} }
func WithChainID(id *big.Int) TxModifier { func (s *stubL1Source) ChainID(_ context.Context) (*big.Int, error) {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey { return chainID, nil
tx.ChainID = id }
return privKey
func (s *stubL1Source) BlockByNumber(_ context.Context, number *big.Int) (*types.Block, error) {
txs, ok := s.txs[number.Uint64()]
if !ok {
return nil, errors.New("not found")
} }
return (&types.Block{}).WithBody(txs, nil), nil
} }
func WithPrivKey(key *ecdsa.PrivateKey) TxModifier { func (s *stubL1Source) TransactionReceipt(_ context.Context, txHash common.Hash) (*types.Receipt, error) {
return func(tx *types.DynamicFeeTx) *ecdsa.PrivateKey { rcptStatus, ok := s.rcptStatus[txHash]
return key if !ok {
rcptStatus = types.ReceiptStatusSuccessful
} else if rcptStatus == MissingReceiptStatus {
return nil, errors.New("not found")
} }
logs := s.logs[txHash]
return &types.Receipt{Status: rcptStatus, Logs: logs}, nil
} }
func (o *stubOracle) txForInput(txMod TxModifier, input keccakTypes.InputData) *types.Transaction { func (s *stubL1Source) createTx(blockNum uint64, key *ecdsa.PrivateKey, txMod TxModifier) *types.Transaction {
id := o.nextTxId txId := s.nextTxId
o.nextTxId++ s.nextTxId++
o.txInputs[id] = input
inner := &types.DynamicFeeTx{ inner := &types.DynamicFeeTx{
ChainID: chainID, ChainID: chainID,
Nonce: 1, Nonce: txId,
To: &oracleAddr, To: &oracleAddr,
Value: big.NewInt(0), Value: big.NewInt(0),
GasTipCap: big.NewInt(1), GasTipCap: big.NewInt(1),
GasFeeCap: big.NewInt(2), GasFeeCap: big.NewInt(2),
Gas: 3, Gas: 3,
Data: []byte{id}, Data: []byte{},
} }
key := txMod(inner) txMod(inner)
tx := types.MustSignNewTx(key, types.LatestSignerForChainID(inner.ChainID), inner) tx := types.MustSignNewTx(key, types.LatestSignerForChainID(inner.ChainID), inner)
// Track tx internally
txSet := s.txs[blockNum]
txSet = append(txSet, tx)
s.txs[blockNum] = txSet
return tx return tx
} }
type stubL1Source struct { func (s *stubL1Source) createLog(tx *types.Transaction, proposal *proposalConfig) *types.Log {
txs map[uint64]types.Transactions // Concat the claimant address and the proposal id
rcptStatus map[common.Hash]uint64 // These will be split back into address and id in fetcher.extractRelevantLeavesFromTx
} data := append(proposal.claimantAddr[:], proposal.id)
func (s *stubL1Source) ChainID(_ context.Context) (*big.Int, error) { txLog := &types.Log{
return chainID, nil Address: oracleAddr,
} Data: data,
Topics: []common.Hash{},
func (s *stubL1Source) BlockByNumber(_ context.Context, number *big.Int) (*types.Block, error) { // ignored (zeroed):
txs, ok := s.txs[number.Uint64()] BlockNumber: 0,
if !ok { TxHash: common.Hash{},
return nil, errors.New("not found") TxIndex: 0,
BlockHash: common.Hash{},
Index: 0,
Removed: false,
} }
return (&types.Block{}).WithBody(txs, nil), nil
}
func (s *stubL1Source) TransactionReceipt(_ context.Context, txHash common.Hash) (*types.Receipt, error) { // Track tx log
rcptStatus, ok := s.rcptStatus[txHash] logSet := s.logs[tx.Hash()]
if !ok { logSet = append(logSet, txLog)
rcptStatus = types.ReceiptStatusSuccessful s.logs[tx.Hash()] = logSet
}
return &types.Receipt{Status: rcptStatus}, nil return txLog
} }
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