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

op-challenger: Cache known valid preimage proposals. (#9345)

Avoids rerequesting the same data repeatedly until the preimage is squeezed.
parent 47d008b2
......@@ -32,6 +32,7 @@ const (
methodChallengeFirstLPP = "challengeFirstLPP"
methodChallengeLPP = "challengeLPP"
methodChallengePeriod = "challengePeriod"
methodGetTreeRootLPP = "getTreeRootLPP"
)
var (
......@@ -190,6 +191,15 @@ func (c *PreimageOracleContract) GetProposalMetadata(ctx context.Context, block
return proposals, nil
}
func (c *PreimageOracleContract) GetProposalTreeRoot(ctx context.Context, block batching.Block, ident keccakTypes.LargePreimageIdent) (common.Hash, error) {
call := c.contract.Call(methodGetTreeRootLPP, ident.Claimant, ident.UUID)
result, err := c.multiCaller.SingleCall(ctx, block, call)
if err != nil {
return common.Hash{}, fmt.Errorf("failed to get tree root: %w", err)
}
return result.GetHash(0), 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),
......
......@@ -185,6 +185,19 @@ func TestGetProposalMetadata(t *testing.T) {
require.Equal(t, []keccakTypes.LargePreimageMetaData{{LargePreimageIdent: ident}}, preimages)
}
func TestGetProposalTreeRoot(t *testing.T) {
blockHash := common.Hash{0xaa}
expectedRoot := common.Hash{0xbb}
ident := keccakTypes.LargePreimageIdent{Claimant: common.Address{0x12}, UUID: big.NewInt(123)}
stubRpc, oracle := setupPreimageOracleTest(t)
stubRpc.SetResponse(oracleAddr, methodGetTreeRootLPP, batching.BlockByHash(blockHash),
[]interface{}{ident.Claimant, ident.UUID},
[]interface{}{expectedRoot})
actualRoot, err := oracle.GetProposalTreeRoot(context.Background(), batching.BlockByHash(blockHash), ident)
require.NoError(t, err)
require.Equal(t, expectedRoot, actualRoot)
}
func setupPreimageOracleTestWithProposals(t *testing.T, block batching.Block) (*batchingTest.AbiBasedRpc, *PreimageOracleContract, []keccakTypes.LargePreimageMetaData) {
stubRpc, oracle := setupPreimageOracleTest(t)
stubRpc.SetResponse(
......
......@@ -6,7 +6,6 @@ import (
"fmt"
"sync"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/matrix"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr"
......@@ -16,7 +15,7 @@ import (
)
type Oracle interface {
fetcher.Oracle
VerifierPreimageOracle
ChallengeTx(ident keccakTypes.LargePreimageIdent, challenge keccakTypes.Challenge) (txmgr.TxCandidate, error)
}
......@@ -26,7 +25,7 @@ type ChallengeMetrics interface {
}
type Verifier interface {
CreateChallenge(ctx context.Context, blockHash common.Hash, oracle fetcher.Oracle, preimage keccakTypes.LargePreimageMetaData) (keccakTypes.Challenge, error)
CreateChallenge(ctx context.Context, blockHash common.Hash, oracle VerifierPreimageOracle, preimage keccakTypes.LargePreimageMetaData) (keccakTypes.Challenge, error)
}
type Sender interface {
......
......@@ -6,7 +6,6 @@ import (
"math/big"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/matrix"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
"github.com/ethereum-optimism/optimism/op-service/testlog"
......@@ -128,7 +127,7 @@ type stubVerifier struct {
err error
}
func (s *stubVerifier) CreateChallenge(_ context.Context, _ common.Hash, _ fetcher.Oracle, preimage keccakTypes.LargePreimageMetaData) (keccakTypes.Challenge, error) {
func (s *stubVerifier) CreateChallenge(_ context.Context, _ common.Hash, _ VerifierPreimageOracle, preimage keccakTypes.LargePreimageMetaData) (keccakTypes.Challenge, error) {
if s.err != nil {
return keccakTypes.Challenge{}, s.err
}
......
......@@ -2,6 +2,7 @@ package keccak
import (
"context"
"errors"
"math/big"
"sync"
"testing"
......@@ -64,6 +65,7 @@ type stubOracle struct {
addr common.Address
getPreimagesCount int
images []keccakTypes.LargePreimageMetaData
treeRoots map[keccakTypes.LargePreimageIdent]common.Hash
}
func (s *stubOracle) GetInputDataBlocks(_ context.Context, _ batching.Block, _ keccakTypes.LargePreimageIdent) ([]uint64, error) {
......@@ -95,6 +97,14 @@ func (s *stubOracle) ChallengeTx(_ keccakTypes.LargePreimageIdent, _ keccakTypes
panic("not supported")
}
func (s *stubOracle) GetProposalTreeRoot(_ context.Context, _ batching.Block, ident keccakTypes.LargePreimageIdent) (common.Hash, error) {
root, ok := s.treeRoots[ident]
if ok {
return root, nil
}
return common.Hash{}, errors.New("unknown tree root")
}
type stubChallenger struct {
m sync.Mutex
checked []keccakTypes.LargePreimageMetaData
......
......@@ -99,6 +99,7 @@ type LargePreimageOracle interface {
Addr() common.Address
GetActivePreimages(ctx context.Context, blockHash common.Hash) ([]LargePreimageMetaData, error)
GetInputDataBlocks(ctx context.Context, block batching.Block, ident LargePreimageIdent) ([]uint64, error)
GetProposalTreeRoot(ctx context.Context, block batching.Block, ident LargePreimageIdent) (common.Hash, error)
DecodeInputData(data []byte) (*big.Int, InputData, error)
ChallengeTx(ident LargePreimageIdent, challenge Challenge) (txmgr.TxCandidate, error)
}
......@@ -3,16 +3,26 @@ package keccak
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/matrix"
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/log"
lru "github.com/hashicorp/golang-lru/v2"
)
const validPreimageCacheSize = 500
type VerifierPreimageOracle interface {
fetcher.Oracle
GetProposalTreeRoot(ctx context.Context, block batching.Block, ident keccakTypes.LargePreimageIdent) (common.Hash, error)
}
type Fetcher interface {
FetchInputs(ctx context.Context, blockHash common.Hash, oracle fetcher.Oracle, ident keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error)
}
......@@ -20,16 +30,36 @@ type Fetcher interface {
type PreimageVerifier struct {
log log.Logger
fetcher Fetcher
// knownValid caches the merkle tree roots that have been confirmed as valid.
// Invalid roots are not cached as those preimages will be ignored once the challenge is processed.
knownValid *lru.Cache[common.Hash, bool]
}
func NewPreimageVerifier(logger log.Logger, fetcher Fetcher) *PreimageVerifier {
// Can't error because size is hard coded
cache, _ := lru.New[common.Hash, bool](validPreimageCacheSize)
return &PreimageVerifier{
log: logger,
fetcher: fetcher,
log: logger,
fetcher: fetcher,
knownValid: cache,
}
}
func (v *PreimageVerifier) CreateChallenge(ctx context.Context, blockHash common.Hash, oracle fetcher.Oracle, preimage keccakTypes.LargePreimageMetaData) (keccakTypes.Challenge, error) {
func (v *PreimageVerifier) CreateChallenge(ctx context.Context, blockHash common.Hash, oracle VerifierPreimageOracle, preimage keccakTypes.LargePreimageMetaData) (keccakTypes.Challenge, error) {
root, err := oracle.GetProposalTreeRoot(ctx, batching.BlockByHash(blockHash), preimage.LargePreimageIdent)
if err != nil {
return keccakTypes.Challenge{}, fmt.Errorf("failed to get proposal merkle root: %w", err)
}
if valid, ok := v.knownValid.Get(root); ok && valid {
// We've already determined that the keccak transition is valid.
// Note that the merkle tree may have been validated by a different proposal but since the tree root
// commits to all the input data and the resulting keccak state matrix, any other proposal with the same
// root must also have the same inputs and correctly applied keccak.
// It is possible that this proposal can't be squeezed because the claimed data length doesn't match the
// actual length but the contracts enforce that and it can't be challenged on that basis.
return keccakTypes.Challenge{}, matrix.ErrValid
}
inputs, err := v.fetcher.FetchInputs(ctx, blockHash, oracle, preimage.LargePreimageIdent)
if err != nil {
return keccakTypes.Challenge{}, fmt.Errorf("failed to fetch leaves: %w", err)
......@@ -41,7 +71,10 @@ func (v *PreimageVerifier) CreateChallenge(ctx context.Context, blockHash common
commitments = append(commitments, input.Commitments...)
}
challenge, err := matrix.Challenge(io.MultiReader(readers...), commitments)
if err != nil {
if errors.Is(err, matrix.ErrValid) {
v.knownValid.Add(root, true)
return keccakTypes.Challenge{}, err
} else if err != nil {
return keccakTypes.Challenge{}, fmt.Errorf("failed to create challenge: %w", err)
}
return challenge, nil
......
......@@ -5,7 +5,9 @@ import (
"context"
"errors"
"io"
"math/big"
"math/rand"
"sync/atomic"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/game/keccak/fetcher"
......@@ -72,7 +74,12 @@ func TestVerify(t *testing.T) {
}
verifier := NewPreimageVerifier(logger, fetcher)
preimage := keccakTypes.LargePreimageMetaData{}
challenge, err := verifier.CreateChallenge(context.Background(), common.Hash{0xff}, &stubOracle{}, preimage)
oracle := &stubOracle{
treeRoots: map[keccakTypes.LargePreimageIdent]common.Hash{
preimage.LargePreimageIdent: {0xde},
},
}
challenge, err := verifier.CreateChallenge(context.Background(), common.Hash{0xff}, oracle, preimage)
require.ErrorIs(t, err, test.expectedErr)
if err == nil {
// Leave checking the validity of the challenge to the StateMatrix tests
......@@ -85,6 +92,48 @@ func TestVerify(t *testing.T) {
}
}
func TestCacheValidRoots(t *testing.T) {
logger := testlog.Logger(t, log.LvlInfo)
fetcher := &stubFetcher{
inputs: validInputs(t, 1),
}
verifier := NewPreimageVerifier(logger, fetcher)
preimage1 := keccakTypes.LargePreimageMetaData{
LargePreimageIdent: keccakTypes.LargePreimageIdent{
Claimant: common.Address{0x12},
UUID: big.NewInt(1),
},
}
preimage2 := keccakTypes.LargePreimageMetaData{
LargePreimageIdent: keccakTypes.LargePreimageIdent{
Claimant: common.Address{0x23},
UUID: big.NewInt(2),
},
}
oracle := &stubOracle{
treeRoots: map[keccakTypes.LargePreimageIdent]common.Hash{
preimage1.LargePreimageIdent: {0xde},
preimage2.LargePreimageIdent: {0xde},
},
}
challenge, err := verifier.CreateChallenge(context.Background(), common.Hash{0xff}, oracle, preimage1)
require.ErrorIs(t, err, matrix.ErrValid)
require.Equal(t, keccakTypes.Challenge{}, challenge, "Should be valid")
require.EqualValues(t, 1, fetcher.fetchCount.Load(), "Should fetch data and validate")
// Should cache the validity
challenge, err = verifier.CreateChallenge(context.Background(), common.Hash{0xee}, oracle, preimage1)
require.ErrorIs(t, err, matrix.ErrValid)
require.Equal(t, keccakTypes.Challenge{}, challenge, "Should be valid")
require.EqualValues(t, 1, fetcher.fetchCount.Load(), "Should use cached validity")
// Should cache the validity across different challenges
challenge, err = verifier.CreateChallenge(context.Background(), common.Hash{0xee}, oracle, preimage2)
require.ErrorIs(t, err, matrix.ErrValid)
require.Equal(t, keccakTypes.Challenge{}, challenge, "Should be valid")
require.EqualValues(t, 1, fetcher.fetchCount.Load(), "Should use cached validity")
}
func validInputs(t *testing.T, inputCount int) []keccakTypes.InputData {
chunkSize := 2 * keccakTypes.BlockSize
data := testutils.RandomData(rand.New(rand.NewSource(4444)), inputCount*chunkSize)
......@@ -105,9 +154,11 @@ func validInputs(t *testing.T, inputCount int) []keccakTypes.InputData {
}
type stubFetcher struct {
inputs []keccakTypes.InputData
inputs []keccakTypes.InputData
fetchCount atomic.Int64
}
func (s *stubFetcher) FetchInputs(_ context.Context, _ common.Hash, _ fetcher.Oracle, _ keccakTypes.LargePreimageIdent) ([]keccakTypes.InputData, error) {
s.fetchCount.Add(1)
return s.inputs, nil
}
......@@ -93,6 +93,10 @@ func TestBondContracts(t *testing.T) {
type stubPreimageOracle common.Address
func (s stubPreimageOracle) GetProposalTreeRoot(_ context.Context, _ batching.Block, _ keccakTypes.LargePreimageIdent) (common.Hash, error) {
panic("not supported")
}
func (s stubPreimageOracle) ChallengeTx(_ keccakTypes.LargePreimageIdent, _ keccakTypes.Challenge) (txmgr.TxCandidate, error) {
panic("not supported")
}
......
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