Skip to content
Merged
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
55 changes: 38 additions & 17 deletions cmd/ethkit/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ import (
)

const (
flagBlockField = "field"
flagBlockFull = "full"
flagBlockRpcUrl = "rpc-url"
flagBlockJson = "json"
flagBlockCheckLogs = "check-logs-bloom"
flagBlockField = "field"
flagBlockFull = "full"
flagBlockRpcUrl = "rpc-url"
flagBlockJson = "json"
flagBlockCheckLogs = "check-logs-bloom"
flagBlockIgnoreZeroGasLogs = "ignore-zero-gas-logs"
)

func init() {
Expand All @@ -48,6 +49,7 @@ func NewBlockCmd() *cobra.Command {
cmd.Flags().StringP(flagBlockRpcUrl, "r", "", "The RPC endpoint to the blockchain node to interact with")
cmd.Flags().BoolP(flagBlockJson, "j", false, "Print the block as JSON")
cmd.Flags().Bool(flagBlockCheckLogs, false, "Check logs bloom against the block header reported value")
cmd.Flags().Bool(flagBlockIgnoreZeroGasLogs, false, "Ignore logs from transactions with gas price 0 when checking bloom (HyperEVM system txs)")

return cmd
}
Expand All @@ -74,6 +76,10 @@ func (c *block) Run(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
fIgnoreZeroGasLogs, err := cmd.Flags().GetBool(flagBlockIgnoreZeroGasLogs)
if err != nil {
return err
}

if _, err = url.ParseRequestURI(fRpc); err != nil {
return errors.New("error: please provide a valid rpc url (e.g. https://nodes.sequence.app/mainnet)")
Expand Down Expand Up @@ -115,7 +121,7 @@ func (c *block) Run(cmd *cobra.Command, args []string) error {
}

if fBlockCheckLogs {
CheckLogs(block, provider)
CheckLogs(block, provider, fIgnoreZeroGasLogs)
}

fmt.Fprintln(cmd.OutOrStdout(), obj)
Expand Down Expand Up @@ -268,7 +274,7 @@ func (b *Block) String() string {
}

// CheckLogs verifies that the logs bloom and logs hash in the block header match the actual logs
func CheckLogs(block *types.Block, provider *ethrpc.Provider) {
func CheckLogs(block *types.Block, provider *ethrpc.Provider, ignoreZeroGasLogs bool) {
h, err := provider.HeaderByNumber(context.Background(), block.Number())

if err != nil {
Expand All @@ -284,23 +290,38 @@ func CheckLogs(block *types.Block, provider *ethrpc.Provider) {
fmt.Println("Error getting logs:", err)
}

filteredLogs := logs
if ignoreZeroGasLogs {
filteredLogs = zeroGasLogsFilter(logs, h, block)
}

fmt.Printf("Block: %d\n", h.Number.Uint64())
fmt.Printf("Logs Count: %d\n", len(logs))
fmt.Printf("Match: %v\n", ethutil.ValidateLogsWithBlockHeader(logs, h))
fmt.Printf("Logs Count: %d\n", len(filteredLogs))
fmt.Printf("Match: %v\n", ethutil.ValidateLogsWithBlockHeader(filteredLogs, h))
fmt.Println()
fmt.Printf("Calculated Log Bloom: 0x%x\n", logsToBloom(logs).Bytes())
fmt.Printf("Calculated Log Bloom: 0x%x\n", ethutil.ConvertLogsToBloom(filteredLogs).Bytes())
fmt.Println()
fmt.Printf("Header Log Bloom: 0x%x\n", h.Bloom.Bytes())
fmt.Println()
}

func logsToBloom(logs []types.Log) types.Bloom {
var logBloom types.Bloom
for _, log := range logs {
logBloom.Add(log.Address.Bytes())
for _, b := range log.Topics {
logBloom.Add(b[:])
// zeroGasLogsFilter removes logs from transactions whose gas price is zero
// (HyperEVM system transactions).
var _ ethutil.LogsFilterFunc = zeroGasLogsFilter

func zeroGasLogsFilter(ls []types.Log, _ *types.Header, block *types.Block) []types.Log {
gasPriceByTx := make(map[common.Hash]*big.Int, len(block.Transactions()))
for _, tx := range block.Transactions() {
gasPriceByTx[tx.Hash()] = tx.GasPrice()
}

out := make([]types.Log, 0, len(ls))
for _, l := range ls {
if gp, ok := gasPriceByTx[l.TxHash]; ok && gp.Sign() == 0 {
// HyperEVM system tx (gas price = 0) — ignore for bloom validation.
continue
}
out = append(out, l)
}
return logBloom
return out
}
19 changes: 14 additions & 5 deletions ethutil/validate_logs_with_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ import (
"github.com/0xsequence/ethkit/go-ethereum/core/types"
)

// ValidateLogsWithBlockHeader validates that the logs comes from given block.
// If the list of logs is not complete or the logs are not from the block, it
// will return false.
// LogsBloomCheckFunc is the shape of a logs bloom validation function.
// Returning true means the logs match the header; false means they do not.
type LogsBloomCheckFunc func(logs []types.Log, header *types.Header) bool

// LogsFilterFunc transforms or filters logs before validation.
// The block is provided for cases where filtering depends on transaction data.
type LogsFilterFunc func(logs []types.Log, header *types.Header, block *types.Block) []types.Log

// ValidateLogsWithBlockHeader validates that the logs come from the given block
// by comparing the calculated bloom against the header bloom.
var _ LogsBloomCheckFunc = ValidateLogsWithBlockHeader

func ValidateLogsWithBlockHeader(logs []types.Log, header *types.Header) bool {
return bytes.Compare(logsToBloom(logs).Bytes(), header.Bloom.Bytes()) == 0
return bytes.Equal(ConvertLogsToBloom(logs).Bytes(), header.Bloom.Bytes())
}

func logsToBloom(logs []types.Log) types.Bloom {
func ConvertLogsToBloom(logs []types.Log) types.Bloom {
var logBloom types.Bloom
for _, log := range logs {
logBloom.Add(log.Address.Bytes())
Expand Down
35 changes: 35 additions & 0 deletions ethutil/validate_logs_with_block_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package ethutil

import (
"bytes"
"context"
"math/big"
"testing"

"github.com/0xsequence/ethkit/ethrpc"
"github.com/0xsequence/ethkit/go-ethereum"
"github.com/0xsequence/ethkit/go-ethereum/common"
"github.com/0xsequence/ethkit/go-ethereum/core/types"
"github.com/stretchr/testify/require"
)

Expand All @@ -26,3 +29,35 @@ func TestValidateLogsWithBlockHeader(t *testing.T) {

require.True(t, ValidateLogsWithBlockHeader(logs, header))
}

func TestValidateLogsWithBlockHeaderWithFilter(t *testing.T) {
logs := []types.Log{
{
Address: common.HexToAddress("0x0000000000000000000000000000000000000001"),
Topics: []common.Hash{common.HexToHash("0x01")},
},
{
Address: common.HexToAddress("0x0000000000000000000000000000000000000002"),
Topics: []common.Hash{common.HexToHash("0x02")},
},
}

headerFull := &types.Header{Bloom: ConvertLogsToBloom(logs)}
headerFiltered := &types.Header{Bloom: ConvertLogsToBloom(logs[1:])}

require.True(t, ValidateLogsWithBlockHeader(logs, headerFull))
require.False(t, ValidateLogsWithBlockHeader(logs, headerFiltered))

var filter LogsFilterFunc = func(ls []types.Log, _ *types.Header, _ *types.Block) []types.Log {
// Ignore the first log (e.g., system tx) and validate bloom against the remainder.
return ls[1:]
}
filteredLogs := filter(logs, headerFiltered, nil)

require.True(t, ValidateLogsWithBlockHeader(filteredLogs, headerFiltered))

var customCheck LogsBloomCheckFunc = func(ls []types.Log, header *types.Header) bool {
return bytes.Equal(ConvertLogsToBloom(ls[1:]).Bytes(), header.Bloom.Bytes())
}
require.True(t, customCheck(logs, headerFiltered))
}
Loading