Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import (

"github.com/coinbase/rosetta-geth-sdk/configuration"
sdkTypes "github.com/coinbase/rosetta-geth-sdk/types"

"github.com/coinbase/rosetta-sdk-go/utils"

RosettaTypes "github.com/coinbase/rosetta-sdk-go/types"
Expand Down
16 changes: 16 additions & 0 deletions configuration/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ type RosettaConfig struct {
// ForwardHeaders is the list of headers to forward to and from the native node
ForwardHeaders []string

// EnableTrustlessBlockValidation determines whether we will try to enable block validation
EnableTrustlessBlockValidation bool

// EnableTrustlessAccountValidation determines whether we will try to enable account validation
EnableTrustlessAccountValidation bool

// Functor to access allowlisted tokens.
// This should be defined in rosetta-xxx implementation if needed
TokenWhitelistAccessor func() ([]Token, error)
Expand Down Expand Up @@ -194,3 +200,13 @@ func (c Configuration) IsAnalyticsMode() bool {
func (c Configuration) IsTokenListEmpty() bool {
return len(c.RosettaCfg.TokenWhiteList) == 0
}

// IsTrustlessBlockValidationEnabled returns true if trustless block validation is enabled
func (c Configuration) IsTrustlessBlockValidationEnabled() bool {
return c.RosettaCfg.EnableTrustlessBlockValidation
}

// IsTrustlessAccountValidationEnabled returns true if trustless account validation is enabled
func (c Configuration) IsTrustlessAccountValidationEnabled() bool {
return c.RosettaCfg.EnableTrustlessAccountValidation
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.36.0
golang.org/x/sync v0.12.0
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
)

require (
Expand Down
47 changes: 47 additions & 0 deletions services/account_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ package services
import (
"context"
"fmt"
"log"
"math/big"
"strings"

"github.com/coinbase/rosetta-geth-sdk/configuration"
AssetTypes "github.com/coinbase/rosetta-geth-sdk/types"
"github.com/ethereum/go-ethereum/common"
"golang.org/x/xerrors"

construction "github.com/coinbase/rosetta-geth-sdk/services/construction"
validator "github.com/coinbase/rosetta-geth-sdk/services/validator"
"github.com/coinbase/rosetta-sdk-go/types"
)

Expand Down Expand Up @@ -76,6 +82,33 @@ func (s *AccountAPIService) AccountBalance(
if err != nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrInternalError, fmt.Errorf("could not get block hash given block identifier %v: %w", request.BlockIdentifier, err))
}
runValidation := s.config.IsTrustlessAccountValidationEnabled()
if runValidation {
log.Printf("Running account validation for block %s and account %s", balanceResponse.BlockIdentifier.Hash, request.AccountIdentifier.Address)
v := validator.NewEthereumValidator(s.config)
addr := common.HexToAddress(request.AccountIdentifier.Address)

result, err := v.GetAccountProof(ctx, addr, big.NewInt(balanceResponse.BlockIdentifier.Index))
if err != nil {
// Check if this is a proof window error - if so, skip validation gracefully
if isProofWindowError(err) {
fmt.Printf("Skipping account validation: block %d is outside proof window\n", balanceResponse.BlockIdentifier.Index)
} else {
return nil, AssetTypes.WrapErr(AssetTypes.ErrGeth, xerrors.Errorf("%w", err))
}
} else {
// Get the state root from the block
stateRoot, err := v.GetBlockStateRoot(ctx, big.NewInt(balanceResponse.BlockIdentifier.Index))
if err != nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrGeth, xerrors.Errorf("failed to get block state root: %w", err))
}

err = v.ValidateAccountState(ctx, result, stateRoot, big.NewInt(balanceResponse.BlockIdentifier.Index))
if err != nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrGeth, err)
}
}
}

return balanceResponse, nil
}
Expand All @@ -87,3 +120,17 @@ func (s *AccountAPIService) AccountCoins(
) (*types.AccountCoinsResponse, *types.Error) {
return nil, AssetTypes.WrapErr(AssetTypes.ErrUnimplemented, nil)
}

// isProofWindowError checks if the error is related to proof window limitations
func isProofWindowError(err error) bool {
if err == nil {
return false
}

// Check for common proof window error messages
errMsg := strings.ToLower(err.Error())
return strings.Contains(errMsg, "distance to target block exceeds maximum proof window") ||
strings.Contains(errMsg, "proof window") ||
strings.Contains(errMsg, "too far from head") ||
strings.Contains(errMsg, "block too old")
}
51 changes: 49 additions & 2 deletions services/block_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

client "github.com/coinbase/rosetta-geth-sdk/client"
construction "github.com/coinbase/rosetta-geth-sdk/services/construction"
validator "github.com/coinbase/rosetta-geth-sdk/services/validator"

RosettaTypes "github.com/coinbase/rosetta-sdk-go/types"
EthTypes "github.com/ethereum/go-ethereum/core/types"
Expand All @@ -38,6 +39,7 @@ import (
AssetTypes "github.com/coinbase/rosetta-geth-sdk/types"

"github.com/coinbase/rosetta-sdk-go/utils"
"github.com/ethereum/go-ethereum/rpc"
)

const (
Expand Down Expand Up @@ -368,10 +370,13 @@ func (s *BlockAPIService) GetBlock(
}
}

return EthTypes.NewBlockWithHeader(&head).WithBody(EthTypes.Body{
log.Printf("Raw: %s", raw)
block := EthTypes.NewBlockWithHeader(&head).WithBody(EthTypes.Body{
Transactions: txs,
Uncles: uncles,
}), loadedTxs, &body, nil
})

return block, loadedTxs, &body, nil
}

// Block implements the /block endpoint.
Expand Down Expand Up @@ -424,6 +429,48 @@ func (s *BlockAPIService) Block(
}
}

// Run validation with full receipts if enabled
runValidation := s.config.IsTrustlessBlockValidationEnabled()
if runValidation && len(loadedTxns) > 0 {
log.Printf("Running validation for block %s", block.Hash().String())
// Fetch full ethtypes.Receipt objects for proper Merkle tree validation
// We need the complete receipt data including Bloom filters
ethReceipts := make(EthTypes.Receipts, len(loadedTxns))
reqs := make([]rpc.BatchElem, len(loadedTxns))

for i, tx := range loadedTxns {
reqs[i] = rpc.BatchElem{
Method: "eth_getTransactionReceipt",
Args: []interface{}{tx.TxHash.String()},
Result: &ethReceipts[i],
}
}

// Execute batch RPC call to get complete receipts
if err := s.client.CallContext(ctx, &ethReceipts, "eth_getBlockReceipts", rpcBlock.Hash); err != nil {
// Fallback to individual receipt fetching if eth_getBlockReceipts is not available
if err := s.client.BatchCallContext(ctx, reqs); err != nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrInternalError, fmt.Errorf("failed to fetch receipts for validation: %w", err))
}

// Check for any errors in the batch
for i := range reqs {
if reqs[i].Error != nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrInternalError, fmt.Errorf("failed to fetch receipt %d: %w", i, reqs[i].Error))
}
if ethReceipts[i] == nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrInternalError, fmt.Errorf("got nil receipt for transaction %d", i))
}
}
}

v := validator.NewEthereumValidator(s.config)
err = v.ValidateBlock(ctx, block, ethReceipts, rpcBlock.Hash)
if err != nil {
return nil, AssetTypes.WrapErr(AssetTypes.ErrInternalError, fmt.Errorf("block validation failed: %w", err))
}
}

blockIdentifier = &RosettaTypes.BlockIdentifier{
Index: block.Number().Int64(),
Hash: block.Hash().String(),
Expand Down
3 changes: 3 additions & 0 deletions services/block_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ func TestBlockService_Offline(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.ModeOffline,
}
os.Setenv("EVM_BLOCK_VALIDATION_ENABLED", "false")

mockClient := &mockedServices.Client{}
servicer := NewBlockAPIService(cfg, mockClient)
ctx := context.Background()
Expand All @@ -82,6 +84,7 @@ func TestBlockService_Online(t *testing.T) {
cfg := &configuration.Configuration{
Mode: configuration.ModeOnline,
}
os.Setenv("EVM_BLOCK_VALIDATION_ENABLED", "false")
mockClient := &mockedServices.Client{}
servicer := NewBlockAPIService(cfg, mockClient)
ctx := context.Background()
Expand Down
Loading
Loading