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 { ...@@ -68,7 +68,9 @@ func (a *Agent) Act(ctx context.Context) error {
for _, action := range actions { for _, action := range actions {
log := a.log.New("action", action.Type, "is_attack", action.IsAttack, "parent", action.ParentIdx) log := a.log.New("action", action.Type, "is_attack", action.IsAttack, "parent", action.ParentIdx)
if action.Type == types.ActionTypeStep { 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 { } else {
log = log.New("value", action.Value) log = log.New("value", action.Value)
} }
......
...@@ -28,6 +28,7 @@ const ( ...@@ -28,6 +28,7 @@ const (
methodProposalMetadata = "proposalMetadata" methodProposalMetadata = "proposalMetadata"
methodProposalBlocksLen = "proposalBlocksLen" methodProposalBlocksLen = "proposalBlocksLen"
methodProposalBlocks = "proposalBlocks" methodProposalBlocks = "proposalBlocks"
methodPreimagePartOk = "preimagePartOk"
) )
var ( var (
...@@ -222,6 +223,15 @@ func (c *PreimageOracleContract) DecodeInputData(data []byte) (*big.Int, keccakT ...@@ -222,6 +223,15 @@ func (c *PreimageOracleContract) DecodeInputData(data []byte) (*big.Int, keccakT
}, nil }, 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 { func (c *PreimageOracleContract) decodePreimageIdent(result *batching.CallResult) keccakTypes.LargePreimageIdent {
return keccakTypes.LargePreimageIdent{ return keccakTypes.LargePreimageIdent{
Claimant: result.GetAddress(0), Claimant: result.GetAddress(0),
......
...@@ -36,6 +36,39 @@ func TestPreimageOracleContract_LoadKeccak256(t *testing.T) { ...@@ -36,6 +36,39 @@ func TestPreimageOracleContract_LoadKeccak256(t *testing.T) {
stubRpc.VerifyTxCandidate(tx) 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) { func TestPreimageOracleContract_InitLargePreimage(t *testing.T) {
stubRpc, oracle := setupPreimageOracleTest(t) stubRpc, oracle := setupPreimageOracleTest(t)
......
...@@ -92,7 +92,7 @@ func NewGamePlayer( ...@@ -92,7 +92,7 @@ func NewGamePlayer(
large := preimages.NewLargePreimageUploader(logger, txMgr, oracle) large := preimages.NewLargePreimageUploader(logger, txMgr, oracle)
uploader := preimages.NewSplitPreimageUploader(direct, large) 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 { if err != nil {
return nil, fmt.Errorf("failed to create the responder: %w", err) return nil, fmt.Errorf("failed to create the responder: %w", err)
} }
......
...@@ -26,6 +26,10 @@ type GameContract interface { ...@@ -26,6 +26,10 @@ type GameContract interface {
GetRequiredBond(ctx context.Context, position types.Position) (*big.Int, error) 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. // FaultResponder implements the [Responder] interface to send onchain transactions.
type FaultResponder struct { type FaultResponder struct {
log log.Logger log log.Logger
...@@ -33,15 +37,17 @@ type FaultResponder struct { ...@@ -33,15 +37,17 @@ type FaultResponder struct {
txMgr txmgr.TxManager txMgr txmgr.TxManager
contract GameContract contract GameContract
uploader preimages.PreimageUploader uploader preimages.PreimageUploader
oracle Oracle
} }
// NewFaultResponder returns a new [FaultResponder]. // 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{ return &FaultResponder{
log: logger, log: logger,
txMgr: txMgr, txMgr: txMgr,
contract: contract, contract: contract,
uploader: uploader, uploader: uploader,
oracle: oracle,
}, nil }, nil
} }
...@@ -78,9 +84,20 @@ func (r *FaultResponder) ResolveClaim(ctx context.Context, claimIdx uint64) erro ...@@ -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 { func (r *FaultResponder) PerformAction(ctx context.Context, action types.Action) error {
if action.OracleData != nil { if action.OracleData != nil {
err := r.uploader.UploadPreimage(ctx, uint64(action.ParentIdx), action.OracleData) var preimageExists bool
if err != nil { var err error
return fmt.Errorf("failed to upload preimage: %w", err) 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 var candidate txmgr.TxCandidate
......
...@@ -22,12 +22,13 @@ var ( ...@@ -22,12 +22,13 @@ var (
mockPreimageUploadErr = errors.New("mock preimage upload error") mockPreimageUploadErr = errors.New("mock preimage upload error")
mockSendError = errors.New("mock send error") mockSendError = errors.New("mock send error")
mockCallError = errors.New("mock call error") mockCallError = errors.New("mock call error")
mockOracleExistsError = errors.New("mock oracle exists error")
) )
// TestCallResolve tests the [Responder.CallResolve]. // TestCallResolve tests the [Responder.CallResolve].
func TestCallResolve(t *testing.T) { func TestCallResolve(t *testing.T) {
t.Run("SendFails", func(t *testing.T) { t.Run("SendFails", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t) responder, _, contract, _, _ := newTestFaultResponder(t)
contract.callFails = true contract.callFails = true
status, err := responder.CallResolve(context.Background()) status, err := responder.CallResolve(context.Background())
require.ErrorIs(t, err, mockCallError) require.ErrorIs(t, err, mockCallError)
...@@ -36,7 +37,7 @@ func TestCallResolve(t *testing.T) { ...@@ -36,7 +37,7 @@ func TestCallResolve(t *testing.T) {
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t) responder, _, contract, _, _ := newTestFaultResponder(t)
status, err := responder.CallResolve(context.Background()) status, err := responder.CallResolve(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, gameTypes.GameStatusInProgress, status) require.Equal(t, gameTypes.GameStatusInProgress, status)
...@@ -47,7 +48,7 @@ func TestCallResolve(t *testing.T) { ...@@ -47,7 +48,7 @@ func TestCallResolve(t *testing.T) {
// TestResolve tests the [Responder.Resolve] method. // TestResolve tests the [Responder.Resolve] method.
func TestResolve(t *testing.T) { func TestResolve(t *testing.T) {
t.Run("SendFails", func(t *testing.T) { t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t) responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
mockTxMgr.sendFails = true mockTxMgr.sendFails = true
err := responder.Resolve(context.Background()) err := responder.Resolve(context.Background())
require.ErrorIs(t, err, mockSendError) require.ErrorIs(t, err, mockSendError)
...@@ -55,7 +56,7 @@ func TestResolve(t *testing.T) { ...@@ -55,7 +56,7 @@ func TestResolve(t *testing.T) {
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t) responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
err := responder.Resolve(context.Background()) err := responder.Resolve(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends) require.Equal(t, 1, mockTxMgr.sends)
...@@ -64,7 +65,7 @@ func TestResolve(t *testing.T) { ...@@ -64,7 +65,7 @@ func TestResolve(t *testing.T) {
func TestCallResolveClaim(t *testing.T) { func TestCallResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) { t.Run("SendFails", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t) responder, _, contract, _, _ := newTestFaultResponder(t)
contract.callFails = true contract.callFails = true
err := responder.CallResolveClaim(context.Background(), 0) err := responder.CallResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockCallError) require.ErrorIs(t, err, mockCallError)
...@@ -72,7 +73,7 @@ func TestCallResolveClaim(t *testing.T) { ...@@ -72,7 +73,7 @@ func TestCallResolveClaim(t *testing.T) {
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
responder, _, contract, _ := newTestFaultResponder(t) responder, _, contract, _, _ := newTestFaultResponder(t)
err := responder.CallResolveClaim(context.Background(), 0) err := responder.CallResolveClaim(context.Background(), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, contract.calls) require.Equal(t, 1, contract.calls)
...@@ -81,7 +82,7 @@ func TestCallResolveClaim(t *testing.T) { ...@@ -81,7 +82,7 @@ func TestCallResolveClaim(t *testing.T) {
func TestResolveClaim(t *testing.T) { func TestResolveClaim(t *testing.T) {
t.Run("SendFails", func(t *testing.T) { t.Run("SendFails", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t) responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
mockTxMgr.sendFails = true mockTxMgr.sendFails = true
err := responder.ResolveClaim(context.Background(), 0) err := responder.ResolveClaim(context.Background(), 0)
require.ErrorIs(t, err, mockSendError) require.ErrorIs(t, err, mockSendError)
...@@ -89,7 +90,7 @@ func TestResolveClaim(t *testing.T) { ...@@ -89,7 +90,7 @@ func TestResolveClaim(t *testing.T) {
}) })
t.Run("Success", func(t *testing.T) { t.Run("Success", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t) responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
err := responder.ResolveClaim(context.Background(), 0) err := responder.ResolveClaim(context.Background(), 0)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 1, mockTxMgr.sends) require.Equal(t, 1, mockTxMgr.sends)
...@@ -99,7 +100,7 @@ func TestResolveClaim(t *testing.T) { ...@@ -99,7 +100,7 @@ func TestResolveClaim(t *testing.T) {
// TestRespond tests the [Responder.Respond] method. // TestRespond tests the [Responder.Respond] method.
func TestPerformAction(t *testing.T) { func TestPerformAction(t *testing.T) {
t.Run("send fails", func(t *testing.T) { t.Run("send fails", func(t *testing.T) {
responder, mockTxMgr, _, _ := newTestFaultResponder(t) responder, mockTxMgr, _, _, _ := newTestFaultResponder(t)
mockTxMgr.sendFails = true mockTxMgr.sendFails = true
err := responder.PerformAction(context.Background(), types.Action{ err := responder.PerformAction(context.Background(), types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
...@@ -112,7 +113,7 @@ func TestPerformAction(t *testing.T) { ...@@ -112,7 +113,7 @@ func TestPerformAction(t *testing.T) {
}) })
t.Run("sends response", func(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{ err := responder.PerformAction(context.Background(), types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
ParentIdx: 123, ParentIdx: 123,
...@@ -124,7 +125,7 @@ func TestPerformAction(t *testing.T) { ...@@ -124,7 +125,7 @@ func TestPerformAction(t *testing.T) {
}) })
t.Run("attack", func(t *testing.T) { t.Run("attack", func(t *testing.T) {
responder, mockTxMgr, contract, _ := newTestFaultResponder(t) responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t)
action := types.Action{ action := types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
ParentIdx: 123, ParentIdx: 123,
...@@ -140,7 +141,7 @@ func TestPerformAction(t *testing.T) { ...@@ -140,7 +141,7 @@ func TestPerformAction(t *testing.T) {
}) })
t.Run("defend", func(t *testing.T) { t.Run("defend", func(t *testing.T) {
responder, mockTxMgr, contract, _ := newTestFaultResponder(t) responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t)
action := types.Action{ action := types.Action{
Type: types.ActionTypeMove, Type: types.ActionTypeMove,
ParentIdx: 123, ParentIdx: 123,
...@@ -156,7 +157,7 @@ func TestPerformAction(t *testing.T) { ...@@ -156,7 +157,7 @@ func TestPerformAction(t *testing.T) {
}) })
t.Run("step", func(t *testing.T) { t.Run("step", func(t *testing.T) {
responder, mockTxMgr, contract, _ := newTestFaultResponder(t) responder, mockTxMgr, contract, _, _ := newTestFaultResponder(t)
action := types.Action{ action := types.Action{
Type: types.ActionTypeStep, Type: types.ActionTypeStep,
ParentIdx: 123, ParentIdx: 123,
...@@ -172,8 +173,8 @@ func TestPerformAction(t *testing.T) { ...@@ -172,8 +173,8 @@ func TestPerformAction(t *testing.T) {
require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData) require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData)
}) })
t.Run("stepWithOracleData", func(t *testing.T) { t.Run("stepWithLocalOracleData", func(t *testing.T) {
responder, mockTxMgr, contract, uploader := newTestFaultResponder(t) responder, mockTxMgr, contract, uploader, oracle := newTestFaultResponder(t)
action := types.Action{ action := types.Action{
Type: types.ActionTypeStep, Type: types.ActionTypeStep,
ParentIdx: 123, ParentIdx: 123,
...@@ -191,10 +192,33 @@ func TestPerformAction(t *testing.T) { ...@@ -191,10 +192,33 @@ func TestPerformAction(t *testing.T) {
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData) require.Equal(t, ([]byte)("step"), mockTxMgr.sent[0].TxData)
require.Equal(t, 1, uploader.updates) 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) { t.Run("stepWithOracleDataAndUploadFails", func(t *testing.T) {
responder, mockTxMgr, contract, uploader := newTestFaultResponder(t) responder, mockTxMgr, contract, uploader, _ := newTestFaultResponder(t)
uploader.uploadFails = true uploader.uploadFails = true
action := types.Action{ action := types.Action{
Type: types.ActionTypeStep, Type: types.ActionTypeStep,
...@@ -212,16 +236,59 @@ func TestPerformAction(t *testing.T) { ...@@ -212,16 +236,59 @@ func TestPerformAction(t *testing.T) {
require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil require.Nil(t, contract.updateOracleArgs) // mock uploader returns nil
require.Equal(t, 1, uploader.updates) 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) log := testlog.Logger(t, log.LvlError)
mockTxMgr := &mockTxManager{} mockTxMgr := &mockTxManager{}
contract := &mockContract{} contract := &mockContract{}
uploader := &mockPreimageUploader{} uploader := &mockPreimageUploader{}
responder, err := NewFaultResponder(log, mockTxMgr, contract, uploader) oracle := &mockOracle{}
responder, err := NewFaultResponder(log, mockTxMgr, contract, uploader, oracle)
require.NoError(t, err) require.NoError(t, err)
return responder, mockTxMgr, contract, uploader return responder, mockTxMgr, contract, uploader, oracle
} }
type mockPreimageUploader struct { type mockPreimageUploader struct {
...@@ -237,6 +304,20 @@ func (m *mockPreimageUploader) UploadPreimage(ctx context.Context, parent uint64 ...@@ -237,6 +304,20 @@ func (m *mockPreimageUploader) UploadPreimage(ctx context.Context, parent uint64
return nil 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 { type mockTxManager struct {
from common.Address from common.Address
sends int sends int
......
...@@ -67,9 +67,18 @@ func NewExecutor(logger log.Logger, m CannonMetricer, cfg *config.Config, inputs ...@@ -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 { 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) 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 { if err != nil {
return fmt.Errorf("find starting snapshot: %w", err) return fmt.Errorf("find starting snapshot: %w", err)
} }
...@@ -82,13 +91,16 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro ...@@ -82,13 +91,16 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
"--output", lastGeneratedState, "--output", lastGeneratedState,
"--meta", "", "--meta", "",
"--info-at", "%" + strconv.FormatUint(uint64(e.infoFreq), 10), "--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"), "--proof-fmt", filepath.Join(proofDir, "%d.json.gz"),
"--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10), "--snapshot-at", "%" + strconv.FormatUint(uint64(e.snapshotFreq), 10),
"--snapshot-fmt", filepath.Join(snapshotDir, "%d.json.gz"), "--snapshot-fmt", filepath.Join(snapshotDir, "%d.json.gz"),
} }
if i < math.MaxUint64 { if end < math.MaxUint64 {
args = append(args, "--stop-at", "="+strconv.FormatUint(i+1, 10)) args = append(args, "--stop-at", "="+strconv.FormatUint(end+1, 10))
}
if untilPreimageRead {
args = append(args, "--stop-at-preimage-type", "global")
} }
args = append(args, args = append(args,
"--", "--",
...@@ -121,9 +133,9 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro ...@@ -121,9 +133,9 @@ func (e *Executor) GenerateProof(ctx context.Context, dir string, i uint64) erro
if err := os.MkdirAll(proofDir, 0755); err != nil { if err := os.MkdirAll(proofDir, 0755); err != nil {
return fmt.Errorf("could not create proofs directory %v: %w", proofDir, err) 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() 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()) e.metrics.RecordCannonExecutionTime(time.Since(execStart).Seconds())
return err return err
} }
......
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"io"
"math"
"os" "os"
"path/filepath" "path/filepath"
...@@ -42,26 +44,24 @@ type ProofGenerator interface { ...@@ -42,26 +44,24 @@ type ProofGenerator interface {
} }
type CannonTraceProvider struct { type CannonTraceProvider struct {
logger log.Logger logger log.Logger
dir string dir string
prestate string prestate string
generator ProofGenerator generator ProofGenerator
gameDepth types.Depth gameDepth types.Depth
localContext common.Hash
// lastStep stores the last step in the actual trace if known. 0 indicates unknown. // 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. // Cached as an optimisation to avoid repeatedly attempting to execute beyond the end of the trace.
lastStep uint64 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{ return &CannonTraceProvider{
logger: logger, logger: logger,
dir: dir, dir: dir,
prestate: cfg.CannonAbsolutePreState, prestate: cfg.CannonAbsolutePreState,
generator: NewExecutor(logger, m, cfg, localInputs), generator: NewExecutor(logger, m, cfg, localInputs),
gameDepth: gameDepth, gameDepth: gameDepth,
localContext: localContext,
} }
} }
...@@ -156,9 +156,9 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa ...@@ -156,9 +156,9 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa
file, err = ioutil.OpenDecompressed(path) file, err = ioutil.OpenDecompressed(path)
if errors.Is(err, os.ErrNotExist) { if errors.Is(err, os.ErrNotExist) {
// Expected proof wasn't generated, check if we reached the end of execution // 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 { if err != nil {
return nil, fmt.Errorf("cannot read final state: %w", err) return nil, err
} }
if state.Exited && state.Step <= i { if state.Exited && state.Step <= i {
p.logger.Warn("Requested proof was after the program exited", "proof", i, "last", state.Step) 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 ...@@ -201,6 +201,14 @@ func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*proofDa
return &proof, nil 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 { type diskStateCacheObj struct {
Step uint64 `json:"step"` Step uint64 `json:"step"`
} }
...@@ -232,3 +240,46 @@ func writeLastStep(dir string, proof *proofData, step uint64) error { ...@@ -232,3 +240,46 @@ func writeLastStep(dir string, proof *proofData, step uint64) error {
} }
return nil 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( ...@@ -37,7 +37,7 @@ func NewOutputCannonTraceAccessor(
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err) 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 return provider, nil
} }
......
...@@ -16,42 +16,49 @@ type ProposalTraceProviderCreator func(ctx context.Context, localContext common. ...@@ -16,42 +16,49 @@ type ProposalTraceProviderCreator func(ctx context.Context, localContext common.
func OutputRootSplitAdapter(topProvider *OutputTraceProvider, creator ProposalTraceProviderCreator) split.ProviderCreator { 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) { return func(ctx context.Context, depth types.Depth, pre types.Claim, post types.Claim) (types.TraceProvider, error) {
localContext := createLocalContext(pre, post) localContext := CreateLocalContext(pre, post)
usePrestateBlock := pre == (types.Claim{}) agreed, disputed, err := FetchProposals(ctx, topProvider, pre, post)
var agreed contracts.Proposal if err != nil {
if usePrestateBlock { return nil, err
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,
}
} }
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 { 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{ agreed = contracts.Proposal{
L2BlockNumber: new(big.Int).SetUint64(postBlockNum), L2BlockNumber: new(big.Int).SetUint64(topProvider.prestateBlock),
OutputRoot: post.Value, OutputRoot: prestateRoot,
} }
} else {
return creator(ctx, localContext, depth, agreed, claimed) 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)) return crypto.Keccak256Hash(localContextPreimage(pre, post))
} }
......
...@@ -84,7 +84,7 @@ func TestOutputRootSplitAdapter(t *testing.T) { ...@@ -84,7 +84,7 @@ func TestOutputRootSplitAdapter(t *testing.T) {
_, err := adapter(context.Background(), 5, preClaim, postClaim) _, err := adapter(context.Background(), 5, preClaim, postClaim)
require.ErrorIs(t, err, creatorError) 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, expectedAgreed, creator.agreed)
require.Equal(t, expectedClaimed, creator.claimed) require.Equal(t, expectedClaimed, creator.claimed)
}) })
...@@ -115,7 +115,7 @@ func TestOutputRootSplitAdapter_FromAbsolutePrestate(t *testing.T) { ...@@ -115,7 +115,7 @@ func TestOutputRootSplitAdapter_FromAbsolutePrestate(t *testing.T) {
_, err := adapter(context.Background(), 5, types.Claim{}, postClaim) _, err := adapter(context.Background(), 5, types.Claim{}, postClaim)
require.ErrorIs(t, err, creatorError) 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, expectedAgreed, creator.agreed)
require.Equal(t, expectedClaimed, creator.claimed) require.Equal(t, expectedClaimed, creator.claimed)
} }
...@@ -204,7 +204,7 @@ func TestCreateLocalContext(t *testing.T) { ...@@ -204,7 +204,7 @@ func TestCreateLocalContext(t *testing.T) {
} }
actualPreimage := localContextPreimage(pre, post) actualPreimage := localContextPreimage(pre, post)
require.Equal(t, test.expected, actualPreimage) require.Equal(t, test.expected, actualPreimage)
localContext := createLocalContext(pre, post) localContext := CreateLocalContext(pre, post)
require.Equal(t, crypto.Keccak256Hash(test.expected), localContext) require.Equal(t, crypto.Keccak256Hash(test.expected), localContext)
}) })
} }
......
...@@ -2,15 +2,18 @@ package disputegame ...@@ -2,15 +2,18 @@ package disputegame
import ( import (
"context" "context"
"crypto/ecdsa"
"fmt" "fmt"
"math/big" "math/big"
"testing" "testing"
"time" "time"
"github.com/ethereum-optimism/optimism/op-bindings/bindings" "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/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/types" "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-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/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
gethtypes "github.com/ethereum/go-ethereum/core/types" gethtypes "github.com/ethereum/go-ethereum/core/types"
...@@ -42,6 +45,10 @@ func (g *OutputGameHelper) SplitDepth(ctx context.Context) types.Depth { ...@@ -42,6 +45,10 @@ func (g *OutputGameHelper) SplitDepth(ctx context.Context) types.Depth {
return types.Depth(splitDepth.Uint64()) 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 { func (g *OutputGameHelper) L2BlockNum(ctx context.Context) uint64 {
blockNum, err := g.game.L2BlockNumber(&bind.CallOpts{Context: ctx}) blockNum, err := g.game.L2BlockNumber(&bind.CallOpts{Context: ctx})
g.require.NoError(err, "failed to load l2 block number") g.require.NoError(err, "failed to load l2 block number")
...@@ -459,6 +466,32 @@ func (g *OutputGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) { ...@@ -459,6 +466,32 @@ func (g *OutputGameHelper) ResolveClaim(ctx context.Context, claimIdx int64) {
g.require.NoError(err, "ResolveClaim transaction was not OK") 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 { func (g *OutputGameHelper) gameData(ctx context.Context) string {
opts := &bind.CallOpts{Context: ctx} opts := &bind.CallOpts{Context: ctx}
maxDepth := g.MaxDepth(ctx) maxDepth := g.MaxDepth(ctx)
......
...@@ -216,6 +216,47 @@ func TestOutputCannonDefendStep(t *testing.T) { ...@@ -216,6 +216,47 @@ func TestOutputCannonDefendStep(t *testing.T) {
require.EqualValues(t, disputegame.StatusChallengerWins, game.Status(ctx)) 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) { func TestOutputCannonProposedOutputRootValid(t *testing.T) {
executor := uint64(1) // Different executor to the other tests to help balance things better 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)) 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