Commit 56751b5b authored by refcell.eth's avatar refcell.eth Committed by GitHub

Validates the absolute prestate of the onchain fault dispute game using (#6676)

the local trace provider.
parent 5e7efe5a
...@@ -8,9 +8,9 @@ import ( ...@@ -8,9 +8,9 @@ import (
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
) )
// ClaimFetcher is a minimal interface around [bindings.FaultDisputeGameCaller]. // MinimalFaultDisputeGameCaller is a minimal interface around [bindings.FaultDisputeGameCaller].
// This needs to be updated if the [bindings.FaultDisputeGameCaller] interface changes. // This needs to be updated if the [bindings.FaultDisputeGameCaller] interface changes.
type ClaimFetcher interface { type MinimalFaultDisputeGameCaller interface {
ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct { ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct {
ParentIndex uint32 ParentIndex uint32
Countered bool Countered bool
...@@ -20,23 +20,25 @@ type ClaimFetcher interface { ...@@ -20,23 +20,25 @@ type ClaimFetcher interface {
}, error) }, error)
ClaimDataLen(opts *bind.CallOpts) (*big.Int, error) ClaimDataLen(opts *bind.CallOpts) (*big.Int, error)
MAXGAMEDEPTH(opts *bind.CallOpts) (*big.Int, error) MAXGAMEDEPTH(opts *bind.CallOpts) (*big.Int, error)
ABSOLUTEPRESTATE(opts *bind.CallOpts) ([32]byte, error)
} }
// Loader is a minimal interface for loading onchain [Claim] data. // Loader is a minimal interface for loading onchain [Claim] data.
type Loader interface { type Loader interface {
FetchClaims(ctx context.Context) ([]types.Claim, error) FetchClaims(ctx context.Context) ([]types.Claim, error)
FetchGameDepth(ctx context.Context) (uint64, error) FetchGameDepth(ctx context.Context) (uint64, error)
FetchAbsolutePrestateHash(ctx context.Context) ([]byte, error)
} }
// loader pulls in fault dispute game claim data periodically and over subscriptions. // loader pulls in fault dispute game claim data periodically and over subscriptions.
type loader struct { type loader struct {
claimFetcher ClaimFetcher caller MinimalFaultDisputeGameCaller
} }
// NewLoader creates a new [loader]. // NewLoader creates a new [loader].
func NewLoader(claimFetcher ClaimFetcher) *loader { func NewLoader(caller MinimalFaultDisputeGameCaller) *loader {
return &loader{ return &loader{
claimFetcher: claimFetcher, caller: caller,
} }
} }
...@@ -46,7 +48,7 @@ func (l *loader) FetchGameDepth(ctx context.Context) (uint64, error) { ...@@ -46,7 +48,7 @@ func (l *loader) FetchGameDepth(ctx context.Context) (uint64, error) {
Context: ctx, Context: ctx,
} }
gameDepth, err := l.claimFetcher.MAXGAMEDEPTH(&callOpts) gameDepth, err := l.caller.MAXGAMEDEPTH(&callOpts)
if err != nil { if err != nil {
return 0, err return 0, err
} }
...@@ -60,7 +62,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim, ...@@ -60,7 +62,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim,
Context: ctx, Context: ctx,
} }
fetchedClaim, err := l.claimFetcher.ClaimData(&callOpts, new(big.Int).SetUint64(arrIndex)) fetchedClaim, err := l.caller.ClaimData(&callOpts, new(big.Int).SetUint64(arrIndex))
if err != nil { if err != nil {
return types.Claim{}, err return types.Claim{}, err
} }
...@@ -78,7 +80,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim, ...@@ -78,7 +80,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim,
if !claim.IsRootPosition() { if !claim.IsRootPosition() {
parentIndex := uint64(fetchedClaim.ParentIndex) parentIndex := uint64(fetchedClaim.ParentIndex)
parentClaim, err := l.claimFetcher.ClaimData(&callOpts, new(big.Int).SetUint64(parentIndex)) parentClaim, err := l.caller.ClaimData(&callOpts, new(big.Int).SetUint64(parentIndex))
if err != nil { if err != nil {
return types.Claim{}, err return types.Claim{}, err
} }
...@@ -94,7 +96,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim, ...@@ -94,7 +96,7 @@ func (l *loader) fetchClaim(ctx context.Context, arrIndex uint64) (types.Claim,
// FetchClaims fetches all claims from the fault dispute game. // FetchClaims fetches all claims from the fault dispute game.
func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) { func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
// Get the current claim count. // Get the current claim count.
claimCount, err := l.claimFetcher.ClaimDataLen(&bind.CallOpts{ claimCount, err := l.caller.ClaimDataLen(&bind.CallOpts{
Context: ctx, Context: ctx,
}) })
if err != nil { if err != nil {
...@@ -113,3 +115,18 @@ func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) { ...@@ -113,3 +115,18 @@ func (l *loader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
return claimList, nil return claimList, nil
} }
// FetchAbsolutePrestateHash fetches the hashed absolute prestate from the fault dispute game.
func (l *loader) FetchAbsolutePrestateHash(ctx context.Context) ([]byte, error) {
callOpts := bind.CallOpts{
Context: ctx,
}
absolutePrestate, err := l.caller.ABSOLUTEPRESTATE(&callOpts)
if err != nil {
return nil, err
}
returnValue := absolutePrestate[:]
return returnValue, nil
}
...@@ -7,7 +7,9 @@ import ( ...@@ -7,7 +7,9 @@ import (
"testing" "testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
...@@ -15,12 +17,14 @@ var ( ...@@ -15,12 +17,14 @@ var (
mockClaimDataError = fmt.Errorf("claim data errored") mockClaimDataError = fmt.Errorf("claim data errored")
mockClaimLenError = fmt.Errorf("claim len errored") mockClaimLenError = fmt.Errorf("claim len errored")
mockMaxGameDepthError = fmt.Errorf("max game depth errored") mockMaxGameDepthError = fmt.Errorf("max game depth errored")
mockPrestateError = fmt.Errorf("prestate errored")
) )
type mockClaimFetcher struct { type mockCaller struct {
claimDataError bool claimDataError bool
claimLenError bool claimLenError bool
maxGameDepthError bool maxGameDepthError bool
prestateError bool
maxGameDepth uint64 maxGameDepth uint64
currentIndex uint64 currentIndex uint64
returnClaims []struct { returnClaims []struct {
...@@ -32,8 +36,8 @@ type mockClaimFetcher struct { ...@@ -32,8 +36,8 @@ type mockClaimFetcher struct {
} }
} }
func newMockClaimFetcher() *mockClaimFetcher { func newMockCaller() *mockCaller {
return &mockClaimFetcher{ return &mockCaller{
returnClaims: []struct { returnClaims: []struct {
ParentIndex uint32 ParentIndex uint32
Countered bool Countered bool
...@@ -63,7 +67,7 @@ func newMockClaimFetcher() *mockClaimFetcher { ...@@ -63,7 +67,7 @@ func newMockClaimFetcher() *mockClaimFetcher {
} }
} }
func (m *mockClaimFetcher) ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct { func (m *mockCaller) ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct {
ParentIndex uint32 ParentIndex uint32
Countered bool Countered bool
Claim [32]byte Claim [32]byte
...@@ -84,46 +88,73 @@ func (m *mockClaimFetcher) ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct ...@@ -84,46 +88,73 @@ func (m *mockClaimFetcher) ClaimData(opts *bind.CallOpts, arg0 *big.Int) (struct
return returnClaim, nil return returnClaim, nil
} }
func (m *mockClaimFetcher) ClaimDataLen(opts *bind.CallOpts) (*big.Int, error) { func (m *mockCaller) ClaimDataLen(opts *bind.CallOpts) (*big.Int, error) {
if m.claimLenError { if m.claimLenError {
return big.NewInt(0), mockClaimLenError return big.NewInt(0), mockClaimLenError
} }
return big.NewInt(int64(len(m.returnClaims))), nil return big.NewInt(int64(len(m.returnClaims))), nil
} }
func (m *mockClaimFetcher) MAXGAMEDEPTH(opts *bind.CallOpts) (*big.Int, error) { func (m *mockCaller) MAXGAMEDEPTH(opts *bind.CallOpts) (*big.Int, error) {
if m.maxGameDepthError { if m.maxGameDepthError {
return nil, mockMaxGameDepthError return nil, mockMaxGameDepthError
} }
return big.NewInt(int64(m.maxGameDepth)), nil return big.NewInt(int64(m.maxGameDepth)), nil
} }
func (m *mockCaller) ABSOLUTEPRESTATE(opts *bind.CallOpts) ([32]byte, error) {
if m.prestateError {
return [32]byte{}, mockPrestateError
}
return common.HexToHash("0xdEad"), nil
}
// TestLoader_FetchGameDepth tests [loader.FetchGameDepth]. // TestLoader_FetchGameDepth tests [loader.FetchGameDepth].
func TestLoader_FetchGameDepth(t *testing.T) { func TestLoader_FetchGameDepth(t *testing.T) {
t.Run("Succeeds", func(t *testing.T) { t.Run("Succeeds", func(t *testing.T) {
mockClaimFetcher := newMockClaimFetcher() mockCaller := newMockCaller()
mockClaimFetcher.maxGameDepth = 10 mockCaller.maxGameDepth = 10
loader := NewLoader(mockClaimFetcher) loader := NewLoader(mockCaller)
depth, err := loader.FetchGameDepth(context.Background()) depth, err := loader.FetchGameDepth(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, uint64(10), depth) require.Equal(t, uint64(10), depth)
}) })
t.Run("Errors", func(t *testing.T) { t.Run("Errors", func(t *testing.T) {
mockClaimFetcher := newMockClaimFetcher() mockCaller := newMockCaller()
mockClaimFetcher.maxGameDepthError = true mockCaller.maxGameDepthError = true
loader := NewLoader(mockClaimFetcher) loader := NewLoader(mockCaller)
depth, err := loader.FetchGameDepth(context.Background()) depth, err := loader.FetchGameDepth(context.Background())
require.ErrorIs(t, mockMaxGameDepthError, err) require.ErrorIs(t, mockMaxGameDepthError, err)
require.Equal(t, depth, uint64(0)) require.Equal(t, depth, uint64(0))
}) })
} }
// TestLoader_FetchAbsolutePrestateHash tests the [loader.FetchAbsolutePrestateHash] function.
func TestLoader_FetchAbsolutePrestateHash(t *testing.T) {
t.Run("Succeeds", func(t *testing.T) {
mockCaller := newMockCaller()
loader := NewLoader(mockCaller)
prestate, err := loader.FetchAbsolutePrestateHash(context.Background())
require.NoError(t, err)
require.ElementsMatch(t, common.HexToHash("0xdEad"), prestate)
})
t.Run("Errors", func(t *testing.T) {
mockCaller := newMockCaller()
mockCaller.prestateError = true
loader := NewLoader(mockCaller)
prestate, err := loader.FetchAbsolutePrestateHash(context.Background())
require.Error(t, err)
require.ElementsMatch(t, common.Hash{}, prestate)
})
}
// TestLoader_FetchClaims_Succeeds tests [loader.FetchClaims]. // TestLoader_FetchClaims_Succeeds tests [loader.FetchClaims].
func TestLoader_FetchClaims_Succeeds(t *testing.T) { func TestLoader_FetchClaims_Succeeds(t *testing.T) {
mockClaimFetcher := newMockClaimFetcher() mockCaller := newMockCaller()
expectedClaims := mockClaimFetcher.returnClaims expectedClaims := mockCaller.returnClaims
loader := NewLoader(mockClaimFetcher) loader := NewLoader(mockCaller)
claims, err := loader.FetchClaims(context.Background()) claims, err := loader.FetchClaims(context.Background())
require.NoError(t, err) require.NoError(t, err)
require.ElementsMatch(t, []types.Claim{ require.ElementsMatch(t, []types.Claim{
...@@ -172,9 +203,9 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) { ...@@ -172,9 +203,9 @@ func TestLoader_FetchClaims_Succeeds(t *testing.T) {
// TestLoader_FetchClaims_ClaimDataErrors tests [loader.FetchClaims] // TestLoader_FetchClaims_ClaimDataErrors tests [loader.FetchClaims]
// when the claim fetcher [ClaimData] function call errors. // when the claim fetcher [ClaimData] function call errors.
func TestLoader_FetchClaims_ClaimDataErrors(t *testing.T) { func TestLoader_FetchClaims_ClaimDataErrors(t *testing.T) {
mockClaimFetcher := newMockClaimFetcher() mockCaller := newMockCaller()
mockClaimFetcher.claimDataError = true mockCaller.claimDataError = true
loader := NewLoader(mockClaimFetcher) loader := NewLoader(mockCaller)
claims, err := loader.FetchClaims(context.Background()) claims, err := loader.FetchClaims(context.Background())
require.ErrorIs(t, err, mockClaimDataError) require.ErrorIs(t, err, mockClaimDataError)
require.Empty(t, claims) require.Empty(t, claims)
...@@ -183,9 +214,9 @@ func TestLoader_FetchClaims_ClaimDataErrors(t *testing.T) { ...@@ -183,9 +214,9 @@ func TestLoader_FetchClaims_ClaimDataErrors(t *testing.T) {
// TestLoader_FetchClaims_ClaimLenErrors tests [loader.FetchClaims] // TestLoader_FetchClaims_ClaimLenErrors tests [loader.FetchClaims]
// when the claim fetcher [ClaimDataLen] function call errors. // when the claim fetcher [ClaimDataLen] function call errors.
func TestLoader_FetchClaims_ClaimLenErrors(t *testing.T) { func TestLoader_FetchClaims_ClaimLenErrors(t *testing.T) {
mockClaimFetcher := newMockClaimFetcher() mockCaller := newMockCaller()
mockClaimFetcher.claimLenError = true mockCaller.claimLenError = true
loader := NewLoader(mockClaimFetcher) loader := NewLoader(mockCaller)
claims, err := loader.FetchClaims(context.Background()) claims, err := loader.FetchClaims(context.Background())
require.ErrorIs(t, err, mockClaimLenError) require.ErrorIs(t, err, mockClaimLenError)
require.Empty(t, claims) require.Empty(t, claims)
......
package fault package fault
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
...@@ -11,6 +12,7 @@ import ( ...@@ -11,6 +12,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/fault/types" "github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum-optimism/optimism/op-service/txmgr" "github.com/ethereum-optimism/optimism/op-service/txmgr"
"github.com/ethereum-optimism/optimism/op-service/txmgr/metrics" "github.com/ethereum-optimism/optimism/op-service/txmgr/metrics"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
...@@ -78,6 +80,10 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*se ...@@ -78,6 +80,10 @@ func NewService(ctx context.Context, logger log.Logger, cfg *config.Config) (*se
// newTypedService creates a new Service from a provided trace provider. // newTypedService creates a new Service from a provided trace provider.
func newTypedService(ctx context.Context, logger log.Logger, cfg *config.Config, loader Loader, gameDepth uint64, client *ethclient.Client, provider types.TraceProvider, updater types.OracleUpdater, txMgr txmgr.TxManager) (*service, error) { func newTypedService(ctx context.Context, logger log.Logger, cfg *config.Config, loader Loader, gameDepth uint64, client *ethclient.Client, provider types.TraceProvider, updater types.OracleUpdater, txMgr txmgr.TxManager) (*service, error) {
if err := ValidateAbsolutePrestate(ctx, provider, loader); err != nil {
return nil, fmt.Errorf("failed to validate absolute prestate: %w", err)
}
gameLogger := logger.New("game", cfg.GameAddress) gameLogger := logger.New("game", cfg.GameAddress)
responder, err := NewFaultResponder(gameLogger, txMgr, cfg.GameAddress) responder, err := NewFaultResponder(gameLogger, txMgr, cfg.GameAddress)
if err != nil { if err != nil {
...@@ -89,16 +95,31 @@ func newTypedService(ctx context.Context, logger log.Logger, cfg *config.Config, ...@@ -89,16 +95,31 @@ func newTypedService(ctx context.Context, logger log.Logger, cfg *config.Config,
return nil, fmt.Errorf("failed to bind the fault contract: %w", err) return nil, fmt.Errorf("failed to bind the fault contract: %w", err)
} }
agent := NewAgent(loader, int(gameDepth), provider, responder, updater, cfg.AgreeWithProposedOutput, gameLogger)
return &service{ return &service{
agent: agent, agent: NewAgent(loader, int(gameDepth), provider, responder, updater, cfg.AgreeWithProposedOutput, gameLogger),
agreeWithProposedOutput: cfg.AgreeWithProposedOutput, agreeWithProposedOutput: cfg.AgreeWithProposedOutput,
caller: caller, caller: caller,
logger: gameLogger, logger: gameLogger,
}, nil }, nil
} }
// ValidateAbsolutePrestate validates the absolute prestate of the fault game.
func ValidateAbsolutePrestate(ctx context.Context, trace types.TraceProvider, loader Loader) error {
providerPrestate, err := trace.AbsolutePreState(ctx)
if err != nil {
return fmt.Errorf("failed to get the trace provider's absolute prestate: %w", err)
}
providerPrestateHash := crypto.Keccak256(providerPrestate)
onchainPrestate, err := loader.FetchAbsolutePrestateHash(ctx)
if err != nil {
return fmt.Errorf("failed to get the onchain absolute prestate: %w", err)
}
if !bytes.Equal(providerPrestateHash, onchainPrestate) {
return fmt.Errorf("trace provider's absolute prestate does not match onchain absolute prestate")
}
return nil
}
// MonitorGame monitors the fault dispute game and attempts to progress it. // MonitorGame monitors the fault dispute game and attempts to progress it.
func (s *service) MonitorGame(ctx context.Context) error { func (s *service) MonitorGame(ctx context.Context) error {
return MonitorGame(ctx, s.logger, s.agreeWithProposedOutput, s.agent, s.caller) return MonitorGame(ctx, s.logger, s.agreeWithProposedOutput, s.agent, s.caller)
......
package fault
import (
"context"
"fmt"
"testing"
"github.com/ethereum-optimism/optimism/op-challenger/fault/types"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/require"
)
var (
mockTraceProviderError = fmt.Errorf("mock trace provider error")
mockLoaderError = fmt.Errorf("mock loader error")
)
// TestValidateAbsolutePrestate tests that the absolute prestate is validated
// correctly by the service component.
func TestValidateAbsolutePrestate(t *testing.T) {
t.Run("ValidPrestates", func(t *testing.T) {
prestate := []byte{0x00, 0x01, 0x02, 0x03}
prestateHash := crypto.Keccak256(prestate)
mockTraceProvider := newMockTraceProvider(false, prestate)
mockLoader := newMockLoader(false, prestateHash)
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.NoError(t, err)
})
t.Run("TraceProviderErrors", func(t *testing.T) {
prestate := []byte{0x00, 0x01, 0x02, 0x03}
mockTraceProvider := newMockTraceProvider(true, prestate)
mockLoader := newMockLoader(false, prestate)
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.ErrorIs(t, err, mockTraceProviderError)
})
t.Run("LoaderErrors", func(t *testing.T) {
prestate := []byte{0x00, 0x01, 0x02, 0x03}
mockTraceProvider := newMockTraceProvider(false, prestate)
mockLoader := newMockLoader(true, prestate)
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.ErrorIs(t, err, mockLoaderError)
})
t.Run("PrestateMismatch", func(t *testing.T) {
mockTraceProvider := newMockTraceProvider(false, []byte{0x00, 0x01, 0x02, 0x03})
mockLoader := newMockLoader(false, []byte{0x00})
err := ValidateAbsolutePrestate(context.Background(), mockTraceProvider, mockLoader)
require.Error(t, err)
})
}
type mockTraceProvider struct {
prestateErrors bool
prestate []byte
}
func newMockTraceProvider(prestateErrors bool, prestate []byte) *mockTraceProvider {
return &mockTraceProvider{
prestateErrors: prestateErrors,
prestate: prestate,
}
}
func (m *mockTraceProvider) Get(ctx context.Context, i uint64) (common.Hash, error) {
panic("not implemented")
}
func (m *mockTraceProvider) GetOracleData(ctx context.Context, i uint64) (*types.PreimageOracleData, error) {
panic("not implemented")
}
func (m *mockTraceProvider) GetPreimage(ctx context.Context, i uint64) (preimage []byte, proofData []byte, err error) {
panic("not implemented")
}
func (m *mockTraceProvider) AbsolutePreState(ctx context.Context) ([]byte, error) {
if m.prestateErrors {
return nil, mockTraceProviderError
}
return m.prestate, nil
}
type mockLoader struct {
prestateError bool
prestate []byte
}
func newMockLoader(prestateError bool, prestate []byte) *mockLoader {
return &mockLoader{
prestateError: prestateError,
prestate: prestate,
}
}
func (m *mockLoader) FetchClaims(ctx context.Context) ([]types.Claim, error) {
panic("not implemented")
}
func (m *mockLoader) FetchGameDepth(ctx context.Context) (uint64, error) {
panic("not implemented")
}
func (m *mockLoader) FetchAbsolutePrestateHash(ctx context.Context) ([]byte, error) {
if m.prestateError {
return nil, mockLoaderError
}
return m.prestate, nil
}
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