Commit 590ba0a9 authored by Inphi's avatar Inphi Committed by GitHub

challenger: e2e cannon test with preimage (#9129)

* challenger: e2e cannon test with preimage

Add e2e test for the preimage upload path

* cleanup

* use rabbitai suggestion; existence check via preimagePartOk

* s/PreimageDataExists/GlobalDataExists/g

* review comments; bundle bisect challenger logic

* op-challenger: Avoid loading already existing global preimages (#9175)

* op-challenger: Add test for pre-existing preimage during step

* challenger: Avoid uploading pre-existing global preimages
parent 94cec943
......@@ -68,7 +68,9 @@ func (a *Agent) Act(ctx context.Context) error {
for _, action := range actions {
log := a.log.New("action", action.Type, "is_attack", action.IsAttack, "parent", action.ParentIdx)
if action.Type == types.ActionTypeStep {
log = log.New("prestate", common.Bytes2Hex(action.PreState), "proof", common.Bytes2Hex(action.ProofData))
containsOracleData := action.OracleData != nil
isLocal := containsOracleData && action.OracleData.IsLocal
log = log.New("prestate", common.Bytes2Hex(action.PreState), "proof", common.Bytes2Hex(action.ProofData), "containsOracleData", containsOracleData, "isLocalPreimage", isLocal)
} else {
log = log.New("value", action.Value)
}
......
......@@ -28,6 +28,7 @@ const (
methodProposalMetadata = "proposalMetadata"
methodProposalBlocksLen = "proposalBlocksLen"
methodProposalBlocks = "proposalBlocks"
methodPreimagePartOk = "preimagePartOk"
)
var (
......@@ -222,6 +223,15 @@ func (c *PreimageOracleContract) DecodeInputData(data []byte) (*big.Int, keccakT
}, nil
}
func (c *PreimageOracleContract) GlobalDataExists(ctx context.Context, data *types.PreimageOracleData) (bool, error) {
call := c.contract.Call(methodPreimagePartOk, common.Hash(data.OracleKey), new(big.Int).SetUint64(uint64(data.OracleOffset)))
results, err := c.multiCaller.SingleCall(ctx, batching.BlockLatest, call)
if err != nil {
return false, fmt.Errorf("failed to get preimagePartOk: %w", err)
}
return results.GetBool(0), nil
}
func (c *PreimageOracleContract) decodePreimageIdent(result *batching.CallResult) keccakTypes.LargePreimageIdent {
return keccakTypes.LargePreimageIdent{
Claimant: result.GetAddress(0),
......
......@@ -36,6 +36,39 @@ func TestPreimageOracleContract_LoadKeccak256(t *testing.T) {
stubRpc.VerifyTxCandidate(tx)
}
func TestPreimageOracleContract_PreimageDataExists(t *testing.T) {
t.Run("exists", func(t *testing.T) {
stubRpc, oracle := setupPreimageOracleTest(t)
data := &types.PreimageOracleData{
OracleKey: common.Hash{0xcc}.Bytes(),
OracleData: make([]byte, 20),
OracleOffset: 545,
}
stubRpc.SetResponse(oracleAddr, methodPreimagePartOk, batching.BlockLatest,
[]interface{}{common.Hash(data.OracleKey), new(big.Int).SetUint64(uint64(data.OracleOffset))},
[]interface{}{true},
)
exists, err := oracle.GlobalDataExists(context.Background(), data)
require.NoError(t, err)
require.True(t, exists)
})
t.Run("does not exist", func(t *testing.T) {
stubRpc, oracle := setupPreimageOracleTest(t)
data := &types.PreimageOracleData{
OracleKey: common.Hash{0xcc}.Bytes(),
OracleData: make([]byte, 20),
OracleOffset: 545,
}
stubRpc.SetResponse(oracleAddr, methodPreimagePartOk, batching.BlockLatest,
[]interface{}{common.Hash(data.OracleKey), new(big.Int).SetUint64(uint64(data.OracleOffset))},
[]interface{}{false},
)
exists, err := oracle.GlobalDataExists(context.Background(), data)
require.NoError(t, err)
require.False(t, exists)
})
}
func TestPreimageOracleContract_InitLargePreimage(t *testing.T) {
stubRpc, oracle := setupPreimageOracleTest(t)
......
......@@ -92,7 +92,7 @@ func NewGamePlayer(
large := preimages.NewLargePreimageUploader(logger, txMgr, oracle)
uploader := preimages.NewSplitPreimageUploader(direct, large)
responder, err := responder.NewFaultResponder(logger, txMgr, loader, uploader)
responder, err := responder.NewFaultResponder(logger, txMgr, loader, uploader, oracle)
if err != nil {
return nil, fmt.Errorf("failed to create the responder: %w", err)
}
......
......@@ -26,6 +26,10 @@ type GameContract interface {
GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error)
}
type Oracle interface {
GlobalDataExists(ctx context.Context, data *types.PreimageOracleData) (bool, error)
}
// FaultResponder implements the [Responder] interface to send onchain transactions.
type FaultResponder struct {
log log.Logger
......@@ -33,15 +37,17 @@ type FaultResponder struct {
txMgr txmgr.TxManager
contract GameContract
uploader preimages.PreimageUploader
oracle Oracle
}
// NewFaultResponder returns a new [FaultResponder].
func NewFaultResponder(logger log.Logger, txMgr txmgr.TxManager, contract GameContract, uploader preimages.PreimageUploader) (*FaultResponder, error) {
func NewFaultResponder(logger log.Logger, txMgr txmgr.TxManager, contract GameContract, uploader preimages.PreimageUploader, oracle Oracle) (*FaultResponder, error) {
return &FaultResponder{
log: logger,
txMgr: txMgr,
contract: contract,
uploader: uploader,
oracle: oracle,
}, nil
}
......@@ -78,9 +84,20 @@ func (r *FaultResponder) ResolveClaim(ctx context.Context, claimIdx uint64) erro
func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error {
if action.OracleData != nil {
err := r.uploader.UploadPreimage(ctx, uint64(action.ParentIdx), action.OracleData)
if err != nil {
return fmt.Errorf("failed to upload preimage: %w", err)
var preimageExists bool
var err error
if !action.OracleData.IsLocal {
preimageExists, err = r.oracle.GlobalDataExists(ctx, action.OracleData)
if err != nil {
return fmt.Errorf("failed to check if preimage exists: %w", err)
}
}
// Always upload local preimages
if !preimageExists {
err := r.uploader.UploadPreimage(ctx, uint64(action.ParentIdx), action.OracleData)
if err != nil {
return fmt.Errorf("failed to upload preimage: %w", err)
}
}
}
var candidate txmgr.TxCandidate
......
......@@ -22,12 +22,13 @@ var (
mockPreimageUploadErr = errors.New("mock preimage upload error")
mockSendError = errors.New("mock send error")
mockCallError = errors.New("mock call error")
mockOracleExistsError = errors.New("mock oracle exists error")
)
// TestCallResolve tests the [Responder.CallResolve].
func TestCallResolve(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t)
responder, _, contract, _, _ := newTestFaultResponder(t)
contract.callFails = true
status, err := responder.CallResolve(context.Background())
require.ErrorIs(t, err, mockCallError)
......@@ -36,7 +37,7 @@ func TestCallResolve(t *testing.T) {
})
t.Run("Success", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t)
responder, _, contract, _, _ := newTestFaultResponder(t)
status, err := responder.CallResolve(context.Background())
require.NoError(t, err)
require.Equal(t, gameTypes.GameStatusInProgress, status)
......@@ -47,7 +48,7 @@ func TestCallResolve(t *testing.T) {
// TestResolve tests the [Responder.Resolve] method.
func TestResolve(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t)
responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
mockTxMgr.sendFails = true
err := responder.Resolve(context.Background())
require.ErrorIs(t, err, mockSendError)
......@@ -55,7 +56,7 @@ func TestResolve(t *testing.T) {
})
t.Run("Success", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t)
responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
err := responder.Resolve(context.Background())
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends)
......@@ -64,7 +65,7 @@ func TestResolve(t *testing.T) {
func TestCallResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t)
responder, _, contract, _, _ := newTestFaultResponder(t)
contract.callFails = true
err := responder.CallResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockCallError)
......@@ -72,7 +73,7 @@ func TestCallResolveClaim(t *testing.T) {
})
t.Run("Success", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t)
responder, _, contract, _, _ := newTestFaultResponder(t)
err := responder.CallResolveClaim(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, 1, contract.calls)
......@@ -81,7 +82,7 @@ func TestCallResolveClaim(t *testing.T) {
func TestResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t)
responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
mockTxMgr.sendFails = true
err := responder.ResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockSendError)
......@@ -89,7 +90,7 @@ func TestResolveClaim(t *testing.T) {
})
t.Run("Success", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t)
responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
err := responder.ResolveClaim(context.Background(), 0)
require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends)
......@@ -99,7 +100,7 @@ func TestResolveClaim(t *testing.T) {
// TestRespond tests the [Responder.Respond] method.
func TestPerformAction(t *testing.T) {
t.Run("send fails", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t)
responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
mockTxMgr.sendFails = true
err := responder.PerformAction(context.Background(), types.Action{
Type: types.ActionTypeMove,
......@@ -112,7 +113,7 @@ func TestPerformAction(t *testing.T) {
})
t.Run("sends response", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t)
responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
err := responder.PerformAction(context.Background(), types.Action{
Type: types.ActionTypeMove,
ParentIdx: 123,
......@@ -124,7 +125,7 @@ func TestPerformAction(t *testing.T) {
})
t.Run("attack", func(t *testing.T) {
responder, mockTxMgr, contract, _ := newTestFaultResponder(t)
responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t)
action := types.Action{
Type: types.ActionTypeMove,
ParentIdx: 123,
......@@ -140,7 +141,7 @@ func TestPerformAction(t *testing.T) {
})
t.Run("defend", func(t *testing.T) {
responder, mockTxMgr, contract, _ := newTestFaultResponder(t)
responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t)
action := types.Action{
Type: types.ActionTypeMove,
ParentIdx: 123,
......@@ -156,7 +157,7 @@ func TestPerformAction(t *testing.T) {
})
t.Run("step", func(t *testing.T) {
responder, mockTxMgr, contract, _ := newTestFaultResponder(t)
responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t)
action := types.Action{
Type: types.ActionTypeStep,
ParentIdx: 123,
......@@ -172,8 +173,8 @@ func TestPerformAction(t *testing.T) {
require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData)
})
t.Run("stepWithOracleData", func(t *testing.T) {
responder, mockTxMgr, contract, uploader := newTestFaultResponder(t)
t.Run("stepWithLocalOracleData", func(t *testing.T) {
responder, mockTxMgr, contract, uploader, oracle := newTestFaultResponder(t)
action := types.Action{
Type: types.ActionTypeStep,
ParentIdx: 123,
......@@ -191,10 +192,33 @@ func TestPerformAction(t *testing.T) {
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData)
require.Equal(t, 1, uploader.updates)
require.Equal(t, 0, oracle.existCalls)
})
t.Run("stepWithGlobalOracleData", func(t *testing.T) {
responder, mockTxMgr, contract, uploader, oracle := newTestFaultResponder(t)
action := types.Action{
Type: types.ActionTypeStep,
ParentIdx: 123,
IsAttack: true,
PreState: []byte{1, 2, 3},
ProofData: []byte{4, 5, 6},
OracleData: &types.PreimageOracleData{
IsLocal: false,
},
}
err := responder.PerformAction(context.Background(), action)
require.NoError(t, err)
require.Len(t, mockTxMgr.sent, 1)
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData)
require.Equal(t, 1, uploader.updates)
require.Equal(t, 1, oracle.existCalls)
})
t.Run("stepWithOracleDataAndUploadFails", func(t *testing.T) {
responder, mockTxMgr, contract, uploader := newTestFaultResponder(t)
responder, mockTxMgr, contract, uploader, _ := newTestFaultResponder(t)
uploader.uploadFails = true
action := types.Action{
Type: types.ActionTypeStep,
......@@ -212,16 +236,59 @@ func TestPerformAction(t *testing.T) {
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, 1, uploader.updates)
})
t.Run("stepWithOracleDataAndGlobalPreimageAlreadyExists", func(t *testing.T) {
responder, mockTxMgr, contract, uploader, oracle := newTestFaultResponder(t)
oracle.existsResult = true
action := types.Action{
Type: types.ActionTypeStep,
ParentIdx: 123,
IsAttack: true,
PreState: []byte{1, 2, 3},
ProofData: []byte{4, 5, 6},
OracleData: &types.PreimageOracleData{
IsLocal: false,
},
}
err := responder.PerformAction(context.Background(), action)
require.Nil(t, err)
require.Len(t, mockTxMgr.sent, 1)
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, 0, uploader.updates)
require.Equal(t, 1, oracle.existCalls)
})
t.Run("stepWithOracleDataAndGlobalPreimageExistsFails", func(t *testing.T) {
responder, mockTxMgr, contract, uploader, oracle := newTestFaultResponder(t)
oracle.existsFails = true
action := types.Action{
Type: types.ActionTypeStep,
ParentIdx: 123,
IsAttack: true,
PreState: []byte{1, 2, 3},
ProofData: []byte{4, 5, 6},
OracleData: &types.PreimageOracleData{
IsLocal: false,
},
}
err := responder.PerformAction(context.Background(), action)
require.ErrorIs(t, err, mockOracleExistsError)
require.Len(t, mockTxMgr.sent, 0)
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, 0, uploader.updates)
require.Equal(t, 1, oracle.existCalls)
})
}
func newTestFaultResponder(t *testing.T) (*FaultResponder, *mockTxManager, *mockContract, *mockPreimageUploader) {
func newTestFaultResponder(t *testing.T) (*FaultResponder, *mockTxManager, *mockContract, *mockPreimageUploader, *mockOracle) {
log := testlog.Logger(t, log.LvlError)
mockTxMgr := &mockTxManager{}
contract := &mockContract{}
uploader := &mockPreimageUploader{}
responder, err := NewFaultResponder(log, mockTxMgr, contract, uploader)
oracle := &mockOracle{}
responder, err := NewFaultResponder(log, mockTxMgr, contract, uploader, oracle)
require.NoError(t, err)
return responder, mockTxMgr, contract, uploader
return responder, mockTxMgr, contract, uploader, oracle
}
type mockPreimageUploader struct {
......@@ -237,6 +304,20 @@ func (m *mockPreimageUploader) UploadPreimage(ctx context.Context, parent uint64
return nil
}
type mockOracle struct {
existCalls int
existsResult bool
existsFails bool
}
func (m *mockOracle) GlobalDataExists(ctx context.Context, data *types.PreimageOracleData) (bool, error) {
m.existCalls++
if m.existsFails {
return false, mockOracleExistsError
}
return m.existsResult, nil
}
type mockTxManager struct {
from common.Address
sends int
......
......@@ -67,9 +67,18 @@ func NewExecutor(logger log.Logger, m CannonMetricer, cfg *config.Config, inputs
}
}
// GenerateProof executes cannon to generate a proof at the specified trace index.
// The proof is stored at the specified directory.
func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) error {
return e.generateProofOrUntilPreimageRead(ctx, dir, i, i, false)
}
// generateProofOrUntilPreimageRead executes cannon to generate a proof at the specified trace index,
// or until a non-local preimage read is encountered if untilPreimageRead is true.
// The proof is stored at the specified directory.
func (e *Executor) generateProofOrUntilPreimageRead(ctx context.Context, dir string, begin uint64, end uint64, untilPreimageRead bool) error {
snapshotDir := filepath.Join(dir, snapsDir)
start, err := e.selectSnapshot(e.logger, snapshotDir, e.absolutePreState, i)
start, err := e.selectSnapshot(e.logger, snapshotDir, e.absolutePreState, begin)
if err != nil {
return fmt.Errorf("find starting snapshot: %w", err)
}
......@@ -82,13 +91,16 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
"--output", lastGeneratedState,
"--meta", "",
"--info-at", "%" + strconv.FormatUint(uint64(e.infoFreq), 10),
"--proof-at", "=" + strconv.FormatUint(i, 10),
"--proof-at", "=" + strconv.FormatUint(end, 10),
"--proof-fmt", filepath.Join(proofDir, "%d.json.gz"),
"--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10),
"--snapshot-fmt", filepath.Join(snapshotDir, "%d.json.gz"),
}
if i < math.MaxUint64 {
args = append(args, "--stop-at", "="+strconv.FormatUint(i+1, 10))
if end < math.MaxUint64 {
args = append(args, "--stop-at", "="+strconv.FormatUint(end+1, 10))
}
if untilPreimageRead {
args = append(args, "--stop-at-preimage-type", "global")
}
args = append(args,
"--",
......@@ -121,9 +133,9 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
if err := os.MkdirAll(proofDir, 0755); err != nil {
return fmt.Errorf("could not create proofs directory %v: %w", proofDir, err)
}
e.logger.Info("Generating trace", "proof", i, "cmd", e.cannon, "args", strings.Join(args, ", "))
e.logger.Info("Generating trace", "proof", end, "cmd", e.cannon, "args", strings.Join(args, ", "))
execStart := time.Now()
err = e.cmdExecutor(ctx, e.logger.New("proof", i), e.cannon, args...)
err = e.cmdExecutor(ctx, e.logger.New("proof", end), e.cannon, args...)
e.metrics.RecordCannonExecutionTime(time.Since(execStart).Seconds())
return err
}
......
......@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
......@@ -42,26 +44,24 @@ type ProofGenerator interface {
}
type CannonTraceProvider struct {
logger log.Logger
dir string
prestate string
generator ProofGenerator
gameDepth types.Depth
localContext common.Hash
logger log.Logger
dir string
prestate string
generator ProofGenerator
gameDepth types.Depth
// lastStep stores the last step in the actual trace if known. 0 indicates unknown.
// Cached as an optimisation to avoid repeatedly attempting to execute beyond the end of the trace.
lastStep uint64
}
func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, localContext common.Hash, localInputs LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProvider {
func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, localInputs LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProvider {
return &CannonTraceProvider{
logger: logger,
dir: dir,
prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, m, cfg, localInputs),
gameDepth: gameDepth,
localContext: localContext,
logger: logger,
dir: dir,
prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, m, cfg, localInputs),
gameDepth: gameDepth,
}
}
......@@ -156,9 +156,9 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa
file, err = ioutil.OpenDecompressed(path)
if errors.Is(err, os.ErrNotExist) {
// Expected proof wasn't generated, check if we reached the end of execution
state, err := parseState(filepath.Join(p.dir, finalState))
state, err := p.finalState()
if err != nil {
return nil, fmt.Errorf("cannot read final state: %w", err)
return nil, err
}
if state.Exited && state.Step <= i {
p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", state.Step)
......@@ -201,6 +201,14 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa
return &proof, nil
}
func (c *CannonTraceProvider) finalState() (*mipsevm.State, error) {
state, err := parseState(filepath.Join(c.dir, finalState))
if err != nil {
return nil, fmt.Errorf("cannot read final state: %w", err)
}
return state, nil
}
type diskStateCacheObj struct {
Step uint64 `json:"step"`
}
......@@ -232,3 +240,46 @@ func writeLastStep(dir string, proof *proofData, step uint64) error {
}
return nil
}
// CannonTraceProviderForTest is a CannonTraceProvider that can find the step referencing the preimage read
// Only to be used for testing
type CannonTraceProviderForTest struct {
*CannonTraceProvider
}
func NewTraceProviderForTest(logger log.Logger, m CannonMetricer, cfg *config.Config, localInputs LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProviderForTest {
p := &CannonTraceProvider{
logger: logger,
dir: dir,
prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, m, cfg, localInputs),
gameDepth: gameDepth,
}
return &CannonTraceProviderForTest{p}
}
func (p *CannonTraceProviderForTest) FindStepReferencingPreimage(ctx context.Context, start uint64) (uint64, error) {
// First generate a snapshot of the starting state, so we can snap to it later for the full trace search
prestateProof, err := p.loadProof(ctx, start)
if err != nil {
return 0, err
}
start += 1
for {
if err := p.generator.(*Executor).generateProofOrUntilPreimageRead(ctx, p.dir, start, math.MaxUint64, true); err != nil {
return 0, fmt.Errorf("generate cannon trace (until preimage read) with proof at %d: %w", start, err)
}
state, err := p.finalState()
if err != nil {
return 0, err
}
if state.Exited {
break
}
if state.PreimageOffset != 0 && state.PreimageOffset != prestateProof.OracleOffset {
return state.Step - 1, nil
}
start = state.Step
}
return 0, io.EOF
}
......@@ -37,7 +37,7 @@ func NewOutputCannonTraceAccessor(
if err != nil {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
}
provider := cannon.NewTraceProvider(logger, m, cfg, localContext, localInputs, subdir, depth)
provider := cannon.NewTraceProvider(logger, m, cfg, localInputs, subdir, depth)
return provider, nil
}
......
......@@ -16,42 +16,49 @@ type ProposalTraceProviderCreator func(ctx context.Context, localContext common.
func OutputRootSplitAdapter(topProvider *OutputTraceProvider, creator ProposalTraceProviderCreator) split.ProviderCreator {
return func(ctx context.Context, depth types.Depth, pre types.Claim, post types.Claim) (types.TraceProvider, error) {
localContext := createLocalContext(pre, post)
usePrestateBlock := pre == (types.Claim{})
var agreed contracts.Proposal
if usePrestateBlock {
prestateRoot, err := topProvider.AbsolutePreStateCommitment(ctx)
if err != nil {
return nil, fmt.Errorf("failed to retrieve absolute prestate output root: %w", err)
}
agreed = contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(topProvider.prestateBlock),
OutputRoot: prestateRoot,
}
} else {
preBlockNum, err := topProvider.BlockNumber(pre.Position)
if err != nil {
return nil, fmt.Errorf("unable to calculate pre-claim block number: %w", err)
}
agreed = contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(preBlockNum),
OutputRoot: pre.Value,
}
localContext := CreateLocalContext(pre, post)
agreed, disputed, err := FetchProposals(ctx, topProvider, pre, post)
if err != nil {
return nil, err
}
postBlockNum, err := topProvider.BlockNumber(post.Position)
return creator(ctx, localContext, depth, agreed, disputed)
}
}
func FetchProposals(ctx context.Context, topProvider *OutputTraceProvider, pre types.Claim, post types.Claim) (contracts.Proposal, contracts.Proposal, error) {
usePrestateBlock := pre == (types.Claim{})
var agreed contracts.Proposal
if usePrestateBlock {
prestateRoot, err := topProvider.AbsolutePreStateCommitment(ctx)
if err != nil {
return nil, fmt.Errorf("unable to calculate post-claim block number: %w", err)
return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("failed to retrieve absolute prestate output root: %w", err)
}
claimed := contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(postBlockNum),
OutputRoot: post.Value,
agreed = contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(topProvider.prestateBlock),
OutputRoot: prestateRoot,
}
return creator(ctx, localContext, depth, agreed, claimed)
} else {
preBlockNum, err := topProvider.BlockNumber(pre.Position)
if err != nil {
return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate pre-claim block number: %w", err)
}
agreed = contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(preBlockNum),
OutputRoot: pre.Value,
}
}
postBlockNum, err := topProvider.BlockNumber(post.Position)
if err != nil {
return contracts.Proposal{}, contracts.Proposal{}, fmt.Errorf("unable to calculate post-claim block number: %w", err)
}
claimed := contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(postBlockNum),
OutputRoot: post.Value,
}
return agreed, claimed, nil
}
func createLocalContext(pre types.Claim, post types.Claim) common.Hash {
func CreateLocalContext(pre types.Claim, post types.Claim) common.Hash {
return crypto.Keccak256Hash(localContextPreimage(pre, post))
}
......
......@@ -84,7 +84,7 @@ func TestOutputRootSplitAdapter(t *testing.T) {
_, err := adapter(context.Background(), 5, preClaim, postClaim)
require.ErrorIs(t, err, creatorError)
require.Equal(t, createLocalContext(preClaim, postClaim), creator.localContext)
require.Equal(t, CreateLocalContext(preClaim, postClaim), creator.localContext)
require.Equal(t, expectedAgreed, creator.agreed)
require.Equal(t, expectedClaimed, creator.claimed)
})
......@@ -115,7 +115,7 @@ func TestOutputRootSplitAdapter_FromAbsolutePrestate(t *testing.T) {
_, err := adapter(context.Background(), 5, types.Claim{}, postClaim)
require.ErrorIs(t, err, creatorError)
require.Equal(t, createLocalContext(types.Claim{}, postClaim), creator.localContext)
require.Equal(t, CreateLocalContext(types.Claim{}, postClaim), creator.localContext)
require.Equal(t, expectedAgreed, creator.agreed)
require.Equal(t, expectedClaimed, creator.claimed)
}
......@@ -204,7 +204,7 @@ func TestCreateLocalContext(t *testing.T) {
}
actualPreimage := localContextPreimage(pre, post)
require.Equal(t, test.expected, actualPreimage)
localContext := createLocalContext(pre, post)
localContext := CreateLocalContext(pre, post)
require.Equal(t, crypto.Keccak256Hash(test.expected), localContext)
})
}
......
......@@ -2,14 +2,19 @@ package disputegame
import (
"context"
"crypto/ecdsa"
"math/big"
"path/filepath"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-challenger/metrics"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/challenger"
"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/log"
)
......@@ -39,11 +44,7 @@ func (g *OutputCannonGameHelper) StartChallenger(
}
func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node string, options ...challenger.Option) *OutputHonestHelper {
opts := []challenger.Option{
challenger.WithCannon(g.t, g.system.RollupCfg(), g.system.L2Genesis(), g.system.RollupEndpoint(l2Node), g.system.NodeEndpoint(l2Node)),
challenger.WithFactoryAddress(g.factoryAddr),
challenger.WithGameAddress(g.addr),
}
opts := g.defaultChallengerOptions(l2Node)
opts = append(opts, options...)
cfg := challenger.NewChallengerConfig(g.t, g.system.NodeEndpoint("l1"), opts...)
......@@ -70,3 +71,156 @@ func (g *OutputCannonGameHelper) CreateHonestActor(ctx context.Context, l2Node s
correctTrace: accessor,
}
}
// ChallengeToFirstGlobalPreimageLoad challenges the supplied execution root claim by inducing a step that requires a preimage to be loaded
// It does this by:
// 1. Identifying the first state transition that loads a global preimage
// 2. Descending the execution game tree to reach the step that loads the preimage
// 3. Asserting that the preimage was indeed loaded by an honest challenger (assuming the preimage is not preloaded)
// This expects an odd execution game depth in order for the honest challenger to step on our leaf claim
func (g *OutputCannonGameHelper) ChallengeToFirstGlobalPreimageLoad(ctx context.Context, outputRootClaim *ClaimHelper, challengerKey *ecdsa.PrivateKey, preloadPreimage bool) {
// 1. Identifying the first state transition that loads a global preimage
provider := g.createCannonTraceProvider(ctx, "sequencer", outputRootClaim, challenger.WithPrivKey(challengerKey))
targetTraceIndex, err := provider.FindStepReferencingPreimage(ctx, 0)
g.require.NoError(err)
splitDepth := g.SplitDepth(ctx)
execDepth := g.ExecDepth(ctx)
g.require.NotEqual(outputRootClaim.position.TraceIndex(execDepth).Uint64(), targetTraceIndex, "cannot move to defend a terminal trace index")
g.require.EqualValues(splitDepth+1, outputRootClaim.Depth(), "supplied claim must be the root of an execution game")
g.require.EqualValues(execDepth%2, 1, "execution game depth must be odd") // since we're challenging the execution root claim
if preloadPreimage {
_, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex))))
g.require.NoError(err)
g.uploadPreimage(ctx, preimageData, challengerKey)
g.require.True(g.preimageExistsInOracle(ctx, preimageData))
}
// 2. Descending the execution game tree to reach the step that loads the preimage
bisectTraceIndex := func(claim *ClaimHelper) *ClaimHelper {
execClaimPosition, err := claim.position.RelativeToAncestorAtDepth(splitDepth + 1)
g.require.NoError(err)
claimTraceIndex := execClaimPosition.TraceIndex(execDepth).Uint64()
g.t.Logf("Bisecting: Into targetTraceIndex %v: claimIndex=%v at depth=%v. claimPosition=%v execClaimPosition=%v claimTraceIndex=%v",
targetTraceIndex, claim.index, claim.Depth(), claim.position, execClaimPosition, claimTraceIndex)
// We always want to position ourselves such that the challenger generates proofs for the targetTraceIndex as prestate
if execClaimPosition.Depth() == execDepth-1 {
if execClaimPosition.TraceIndex(execDepth).Uint64() == targetTraceIndex {
newPosition := execClaimPosition.Attack()
correct, err := provider.Get(ctx, newPosition)
g.require.NoError(err)
g.t.Logf("Bisecting: Attack correctly for step at newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth))
return claim.Attack(ctx, correct)
} else if execClaimPosition.TraceIndex(execDepth).Uint64() > targetTraceIndex {
g.t.Logf("Bisecting: Attack incorrectly for step")
return claim.Attack(ctx, common.Hash{0xdd})
} else if execClaimPosition.TraceIndex(execDepth).Uint64()+1 == targetTraceIndex {
g.t.Logf("Bisecting: Defend incorrectly for step")
return claim.Defend(ctx, common.Hash{0xcc})
} else {
newPosition := execClaimPosition.Defend()
correct, err := provider.Get(ctx, newPosition)
g.require.NoError(err)
g.t.Logf("Bisecting: Defend correctly for step at newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth))
return claim.Defend(ctx, correct)
}
}
// Attack or Defend depending on whether the claim we're responding to is to the left or right of the trace index
// Induce the honest challenger to attack or defend depending on whether our new position will be to the left or right of the trace index
if execClaimPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex && claim.Depth() != splitDepth+1 {
newPosition := execClaimPosition.Defend()
if newPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex {
g.t.Logf("Bisecting: Defend correct. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth))
correct, err := provider.Get(ctx, newPosition)
g.require.NoError(err)
return claim.Defend(ctx, correct)
} else {
g.t.Logf("Bisecting: Defend incorrect. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth))
return claim.Defend(ctx, common.Hash{0xaa})
}
} else {
newPosition := execClaimPosition.Attack()
if newPosition.TraceIndex(execDepth).Uint64() < targetTraceIndex {
g.t.Logf("Bisecting: Attack correct. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth))
correct, err := provider.Get(ctx, newPosition)
g.require.NoError(err)
return claim.Attack(ctx, correct)
} else {
g.t.Logf("Bisecting: Attack incorrect. newPosition=%v execIndexAtDepth=%v", newPosition, newPosition.TraceIndex(execDepth))
return claim.Attack(ctx, common.Hash{0xbb})
}
}
}
g.LogGameData(ctx)
// Initial bisect to put us on defense
claim := bisectTraceIndex(outputRootClaim)
g.DefendClaim(ctx, claim, bisectTraceIndex)
// 3. Asserts that the preimage was indeed loaded by an honest challenger
_, _, preimageData, err := provider.GetStepData(ctx, types.NewPosition(execDepth, big.NewInt(int64(targetTraceIndex))))
g.require.NoError(err)
g.require.True(g.preimageExistsInOracle(ctx, preimageData))
}
func (g *OutputCannonGameHelper) createCannonTraceProvider(ctx context.Context, l2Node string, outputRootClaim *ClaimHelper, options ...challenger.Option) *cannon.CannonTraceProviderForTest {
splitDepth := g.SplitDepth(ctx)
g.require.EqualValues(outputRootClaim.Depth(), splitDepth+1, "outputRootClaim must be the root of an execution game")
logger := testlog.Logger(g.t, log.LvlInfo).New("role", "CannonTraceProvider", "game", g.addr)
opt := g.defaultChallengerOptions(l2Node)
opt = append(opt, options...)
cfg := challenger.NewChallengerConfig(g.t, g.system.NodeEndpoint("l1"), opt...)
caller := batching.NewMultiCaller(g.system.NodeClient("l1").Client(), batching.DefaultBatchSize)
l2Client := g.system.NodeClient(l2Node)
contract, err := contracts.NewFaultDisputeGameContract(g.addr, caller)
g.require.NoError(err, "Failed to create game contact")
prestateBlock, poststateBlock, err := contract.GetBlockRange(ctx)
g.require.NoError(err, "Failed to load block range")
rollupClient := g.system.RollupClient(l2Node)
prestateProvider := outputs.NewPrestateProvider(ctx, logger, rollupClient, prestateBlock)
outputProvider := outputs.NewTraceProviderFromInputs(logger, prestateProvider, rollupClient, splitDepth, prestateBlock, poststateBlock)
topLeaf := g.getClaim(ctx, int64(outputRootClaim.parentIndex))
topLeafPosition := types.NewPositionFromGIndex(topLeaf.Position)
var pre, post types.Claim
if outputRootClaim.position.TraceIndex(outputRootClaim.Depth()).Cmp(topLeafPosition.TraceIndex(outputRootClaim.Depth())) > 0 {
pre, err = contract.GetClaim(ctx, uint64(outputRootClaim.parentIndex))
g.require.NoError(err, "Failed to construct pre claim")
post, err = contract.GetClaim(ctx, uint64(outputRootClaim.index))
g.require.NoError(err, "Failed to construct post claim")
} else {
post, err = contract.GetClaim(ctx, uint64(outputRootClaim.parentIndex))
postTraceIdx := post.TraceIndex(splitDepth)
if postTraceIdx.Cmp(big.NewInt(0)) == 0 {
pre = types.Claim{}
} else {
g.require.NoError(err, "Failed to construct post claim")
pre, err = contract.GetClaim(ctx, uint64(outputRootClaim.index))
g.require.NoError(err, "Failed to construct pre claim")
}
}
agreed, disputed, err := outputs.FetchProposals(ctx, outputProvider, pre, post)
g.require.NoError(err, "Failed to fetch proposals")
localInputs, err := cannon.FetchLocalInputsFromProposals(ctx, contract, l2Client, agreed, disputed)
g.require.NoError(err, "Failed to fetch local inputs")
localContext := outputs.CreateLocalContext(pre, post)
dir := filepath.Join(cfg.Datadir, "cannon-trace")
subdir := filepath.Join(dir, localContext.Hex())
return cannon.NewTraceProviderForTest(logger, metrics.NoopMetrics, cfg, localInputs, subdir, g.MaxDepth(ctx)-splitDepth-1)
}
func (g *OutputCannonGameHelper) defaultChallengerOptions(l2Node string) []challenger.Option {
return []challenger.Option{
challenger.WithCannon(g.t, g.system.RollupCfg(), g.system.L2Genesis(), g.system.RollupEndpoint(l2Node), g.system.NodeEndpoint(l2Node)),
challenger.WithFactoryAddress(g.factoryAddr),
challenger.WithGameAddress(g.addr),
}
}
......@@ -2,15 +2,18 @@ package disputegame
import (
"context"
"crypto/ecdsa"
"fmt"
"math/big"
"testing"
"time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/contracts"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-service/sources/batching"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types"
......@@ -42,6 +45,10 @@ func (g *OutputGameHelper) SplitDepth(ctx context.Context) types.Depth {
return types.Depth(splitDepth.Uint64())
}
func (g *OutputGameHelper) ExecDepth(ctx context.Context) types.Depth {
return g.MaxDepth(ctx) - g.SplitDepth(ctx) - 1
}
func (g *OutputGameHelper) L2BlockNum(ctx context.Context) uint64 {
blockNum, err := g.game.L2BlockNumber(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "failed to load l2 block number")
......@@ -459,6 +466,32 @@ func (g *OutputGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) {
g.require.NoError(err, "ResolveClaim transaction was not OK")
}
func (g *OutputGameHelper) preimageExistsInOracle(ctx context.Context, data *types.PreimageOracleData) bool {
oracle := g.oracle(ctx)
exists, err := oracle.GlobalDataExists(ctx, data)
g.require.NoError(err)
return exists
}
func (g *OutputGameHelper) uploadPreimage(ctx context.Context, data *types.PreimageOracleData, privateKey *ecdsa.PrivateKey) {
oracle := g.oracle(ctx)
boundOracle, err := bindings.NewPreimageOracle(oracle.Addr(), g.client)
g.require.NoError(err)
tx, err := boundOracle.LoadKeccak256PreimagePart(g.opts, new(big.Int).SetUint64(uint64(data.OracleOffset)), data.GetPreimageWithoutSize())
g.require.NoError(err, "Failed to load preimage part")
_, err = wait.ForReceiptOK(ctx, g.client, tx.Hash())
g.require.NoError(err)
}
func (g *OutputGameHelper) oracle(ctx context.Context) *contracts.PreimageOracleContract {
caller := batching.NewMultiCaller(g.system.NodeClient("l1").Client(), batching.DefaultBatchSize)
contract, err := contracts.NewFaultDisputeGameContract(g.addr, caller)
g.require.NoError(err, "Failed to create game contract")
oracle, err := contract.GetOracle(ctx)
g.require.NoError(err, "Failed to create oracle contract")
return oracle
}
func (g *OutputGameHelper) gameData(ctx context.Context) string {
opts := &bind.CallOpts{Context: ctx}
maxDepth := g.MaxDepth(ctx)
......
......@@ -216,6 +216,47 @@ func TestOutputCannonDefendStep(t *testing.T) {
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
}
func TestOutputCannonStepWithPreimage(t *testing.T) {
executor := uint64(1) // Different executor to the other tests to help balance things better
testPreimageStep := func(t *testing.T, preloadPreimage bool) {
op_e2e.InitParallel(t, op_e2e.UsesCannon, op_e2e.UseExecutor(executor))
ctx := context.Background()
sys, l1Client := startFaultDisputeSystem(t)
t.Cleanup(sys.Close)
disputeGameFactory := disputegame.NewFactoryHelper(t, ctx, sys)
game := disputeGameFactory.StartOutputCannonGame(ctx, "sequencer", 1, common.Hash{0x01, 0xaa})
require.NotNil(t, game)
outputRootClaim := game.DisputeLastBlock(ctx)
game.LogGameData(ctx)
game.StartChallenger(ctx, "sequencer", "Challenger", challenger.WithPrivKey(sys.Cfg.Secrets.Alice))
// Wait for the honest challenger to dispute the outputRootClaim. This creates a root of an execution game that we challenge by coercing
// a step at a preimage trace index.
outputRootClaim = outputRootClaim.WaitForCounterClaim(ctx)
// Now the honest challenger is positioned as the defender of the execution game
// We then move to challenge it to induce a preimage load
game.ChallengeToFirstGlobalPreimageLoad(ctx, outputRootClaim, sys.Cfg.Secrets.Alice, preloadPreimage)
sys.TimeTravelClock.AdvanceTime(game.GameDuration(ctx))
require.NoError(t, wait.ForNextBlock(ctx, l1Client))
game.WaitForInactivity(ctx, 10, true)
game.LogGameData(ctx)
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx))
}
t.Run("non-existing preimage", func(t *testing.T) {
testPreimageStep(t, false)
})
t.Run("preimage already exists", func(t *testing.T) {
testPreimageStep(t, true)
})
}
func TestOutputCannonProposedOutputRootValid(t *testing.T) {
executor := uint64(1) // Different executor to the other tests to help balance things better
op_e2e.InitParallel(t, op_e2e.UsesCannon, op_e2e.UseExecutor(executor))
......
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