Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Timeboost Bulk BlockMetadata API #2754

Open
wants to merge 5 commits into
base: add-timeboosted-broadcastfeedmessage
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions arbnode/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -1034,3 +1034,7 @@ func (n *Node) ValidatedMessageCount() (arbutil.MessageIndex, error) {
}
return n.BlockValidator.GetValidated(), nil
}

func (n *Node) BlockMetadataAtCount(count arbutil.MessageIndex) (arbostypes.BlockMetadata, error) {
return n.TxStreamer.BlockMetadataAtCount(count)
}
23 changes: 20 additions & 3 deletions execution/gethexec/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/ethereum/go-ethereum/arbitrum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/rpc"
Expand All @@ -25,17 +26,33 @@ import (
)

type ArbAPI struct {
txPublisher TransactionPublisher
txPublisher TransactionPublisher
bulkBlockMetadataFetcher *BulkBlockMetadataFetcher
}

func NewArbAPI(publisher TransactionPublisher, bulkBlockMetadataFetcher *BulkBlockMetadataFetcher) *ArbAPI {
return &ArbAPI{
txPublisher: publisher,
bulkBlockMetadataFetcher: bulkBlockMetadataFetcher,
}
}

func NewArbAPI(publisher TransactionPublisher) *ArbAPI {
return &ArbAPI{publisher}
type NumberAndBlockMetadata struct {
BlockNumber uint64 `json:"blockNumber"`
RawMetadata hexutil.Bytes `json:"rawMetadata"`
}

func (a *ArbAPI) CheckPublisherHealth(ctx context.Context) error {
return a.txPublisher.CheckHealth(ctx)
}

func (a *ArbAPI) GetRawBlockMetadata(ctx context.Context, fromBlock, toBlock hexutil.Uint64) ([]NumberAndBlockMetadata, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similarly to (b *BulkBlockMetadataFetcher) Fetch, is there a reason to not use rpc.BlockNumber here?

if a.bulkBlockMetadataFetcher == nil {
return nil, errors.New("arb_getRawBlockMetadata is not available")
}
return a.bulkBlockMetadataFetcher.Fetch(fromBlock, toBlock)
}

type ArbTimeboostAuctioneerAPI struct {
txPublisher TransactionPublisher
}
Expand Down
67 changes: 67 additions & 0 deletions execution/gethexec/blockmetadata.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package gethexec

import (
"fmt"

"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/offchainlabs/nitro/arbos/arbostypes"
"github.com/offchainlabs/nitro/arbutil"
"github.com/offchainlabs/nitro/util/containers"
)

type BlockMetadataFetcher interface {
BlockMetadataAtCount(count arbutil.MessageIndex) (arbostypes.BlockMetadata, error)
BlockNumberToMessageIndex(blockNum uint64) (arbutil.MessageIndex, error)
MessageIndexToBlockNumber(messageNum arbutil.MessageIndex) uint64
}

type BulkBlockMetadataFetcher struct {
fetcher BlockMetadataFetcher
cache *containers.LruCache[arbutil.MessageIndex, arbostypes.BlockMetadata]
}

func NewBulkBlockMetadataFetcher(fetcher BlockMetadataFetcher, cacheSize int) *BulkBlockMetadataFetcher {
var cache *containers.LruCache[arbutil.MessageIndex, arbostypes.BlockMetadata]
if cacheSize != 0 {
cache = containers.NewLruCache[arbutil.MessageIndex, arbostypes.BlockMetadata](cacheSize)
}
return &BulkBlockMetadataFetcher{
fetcher: fetcher,
cache: cache,
}
}

func (b *BulkBlockMetadataFetcher) Fetch(fromBlock, toBlock hexutil.Uint64) ([]NumberAndBlockMetadata, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason for using hexutil.Uint64 here instead of rpc.BlockNumber?

start, err := b.fetcher.BlockNumberToMessageIndex(uint64(fromBlock))
if err != nil {
return nil, fmt.Errorf("error converting fromBlock blocknumber to message index: %w", err)
}
end, err := b.fetcher.BlockNumberToMessageIndex(uint64(toBlock))
if err != nil {
return nil, fmt.Errorf("error converting toBlock blocknumber to message index: %w", err)
}
var result []NumberAndBlockMetadata
for i := start; i <= end; i++ {
var data arbostypes.BlockMetadata
var found bool
if b.cache != nil {
data, found = b.cache.Get(i)
}
if !found {
data, err = b.fetcher.BlockMetadataAtCount(i + 1)
if err != nil {
return nil, err
}
if data != nil && b.cache != nil {
b.cache.Add(i, data)
}
}
if data != nil {
result = append(result, NumberAndBlockMetadata{
BlockNumber: b.fetcher.MessageIndexToBlockNumber(i),
RawMetadata: (hexutil.Bytes)(data),
})
}
}
return result, nil
}
7 changes: 7 additions & 0 deletions execution/gethexec/executionengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ func (s *ExecutionEngine) SetConsensus(consensus execution.FullConsensusClient)
s.consensus = consensus
}

func (s *ExecutionEngine) BlockMetadataAtCount(count arbutil.MessageIndex) (arbostypes.BlockMetadata, error) {
if s.consensus != nil {
return s.consensus.BlockMetadataAtCount(count)
}
return nil, errors.New("FullConsensusClient is not accessible to execution")
}

func (s *ExecutionEngine) GetBatchFetcher() execution.BatchFetcher {
return s.consensus
}
Expand Down
10 changes: 9 additions & 1 deletion execution/gethexec/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Config struct {
EnablePrefetchBlock bool `koanf:"enable-prefetch-block"`
SyncMonitor SyncMonitorConfig `koanf:"sync-monitor"`
StylusTarget StylusTargetConfig `koanf:"stylus-target"`
BlockMetadataApiCacheSize int `koanf:"block-metadata-api-cache-size"`

forwardingTarget string
}
Expand All @@ -82,6 +83,9 @@ func (c *Config) Validate() error {
if c.forwardingTarget != "" && c.Sequencer.Enable {
return errors.New("ForwardingTarget set and sequencer enabled")
}
if c.BlockMetadataApiCacheSize < 0 {
return errors.New("block-metadata-api-cache-size cannot be negative")
}
return nil
}

Expand All @@ -99,6 +103,9 @@ func ConfigAddOptions(prefix string, f *flag.FlagSet) {
f.Uint64(prefix+".tx-lookup-limit", ConfigDefault.TxLookupLimit, "retain the ability to lookup transactions by hash for the past N blocks (0 = all blocks)")
f.Bool(prefix+".enable-prefetch-block", ConfigDefault.EnablePrefetchBlock, "enable prefetching of blocks")
StylusTargetConfigAddOptions(prefix+".stylus-target", f)
f.Int(prefix+".block-metadata-api-cache-size", ConfigDefault.BlockMetadataApiCacheSize, "size of lru cache storing the blockMetadata to service arb_getRawBlockMetadata.\n"+
"Note: setting a non-zero value would mean the blockMetadata might be outdated (if the block was reorged out).\n"+
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we somehow trigger clearing the cache on reorg?

"Default is set to 0 which disables caching")
}

var ConfigDefault = Config{
Expand All @@ -114,6 +121,7 @@ var ConfigDefault = Config{
Forwarder: DefaultNodeForwarderConfig,
EnablePrefetchBlock: true,
StylusTarget: DefaultStylusTargetConfig,
BlockMetadataApiCacheSize: 0,
}

type ConfigFetcher func() *Config
Expand Down Expand Up @@ -221,7 +229,7 @@ func CreateExecutionNode(
apis := []rpc.API{{
Namespace: "arb",
Version: "1.0",
Service: NewArbAPI(txPublisher),
Service: NewArbAPI(txPublisher, NewBulkBlockMetadataFetcher(execEngine, config.BlockMetadataApiCacheSize)),
Public: false,
}}
apis = append(apis, rpc.API{
Expand Down
1 change: 1 addition & 0 deletions execution/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ type ConsensusInfo interface {
Synced() bool
FullSyncProgressMap() map[string]interface{}
SyncTargetMessageCount() arbutil.MessageIndex
BlockMetadataAtCount(count arbutil.MessageIndex) (arbostypes.BlockMetadata, error)

// TODO: switch from pulling to pushing safe/finalized
GetSafeMsgCount(ctx context.Context) (arbutil.MessageIndex, error)
Expand Down
1 change: 1 addition & 0 deletions system_tests/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ func (b *NodeBuilder) RestartL2Node(t *testing.T) {
l2.Client = client
l2.ExecNode = execNode
l2.cleanup = func() { b.L2.ConsensusNode.StopAndWait() }
l2.Stack = stack

b.L2 = l2
b.L2Info = l2info
Expand Down
86 changes: 86 additions & 0 deletions system_tests/timeboost_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package arbtest

import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/binary"
"fmt"
"math/big"
"net"
Expand Down Expand Up @@ -41,6 +43,90 @@ import (
"github.com/stretchr/testify/require"
)

func TestTimeboosBulkBlockMetadataAPI(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timeboos typo :)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

builder := NewNodeBuilder(ctx).DefaultConfig(t, false)
cleanup := builder.Build(t)
defer cleanup()

arbDb := builder.L2.ConsensusNode.ArbDB
dbKey := func(prefix []byte, pos uint64) []byte {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: looks like dbKey is only used here []byte("t") prefix - can we rename it to something like blockMetadataInputFeedKey and take only block number as argument?
or alternatively we might define a const blockMetadataInputFeedPrefix = []byte("t") in the test?

var key []byte
key = append(key, prefix...)
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, pos)
key = append(key, data...)
return key
}

start := 1
end := 20
var sampleBulkData []gethexec.NumberAndBlockMetadata
for i := start; i <= end; i += 2 {
sampleData := gethexec.NumberAndBlockMetadata{
BlockNumber: uint64(i),
RawMetadata: []byte{0, uint8(i)},
}
sampleBulkData = append(sampleBulkData, sampleData)
arbDb.Put(dbKey([]byte("t"), sampleData.BlockNumber), sampleData.RawMetadata)
}

l2rpc := builder.L2.Stack.Attach()
var result []gethexec.NumberAndBlockMetadata
err := l2rpc.CallContext(ctx, &result, "arb_getRawBlockMetadata", hexutil.Uint64(start), hexutil.Uint64(end))
Require(t, err)

if len(result) != len(sampleBulkData) {
t.Fatalf("number of entries in arb_getRawBlockMetadata is incorrect. Got: %d, Want: %d", len(result), len(sampleBulkData))
}
for i, data := range result {
if data.BlockNumber != sampleBulkData[i].BlockNumber {
t.Fatalf("BlockNumber mismatch. Got: %d, Want: %d", data.BlockNumber, sampleBulkData[i].BlockNumber)
}
if !bytes.Equal(data.RawMetadata, sampleBulkData[i].RawMetadata) {
t.Fatalf("RawMetadata. Got: %s, Want: %s", data.RawMetadata, sampleBulkData[i].RawMetadata)
}
}

// Test that without cache the result returned is always in sync with ArbDB
sampleBulkData[0].RawMetadata = []byte{1, 11}
arbDb.Put(dbKey([]byte("t"), 1), sampleBulkData[0].RawMetadata)

err = l2rpc.CallContext(ctx, &result, "arb_getRawBlockMetadata", hexutil.Uint64(1), hexutil.Uint64(1))
Require(t, err)
if len(result) != 1 {
t.Fatal("result returned with more than one entry")
}
if !bytes.Equal(sampleBulkData[0].RawMetadata, result[0].RawMetadata) {
t.Fatal("BlockMetadata gotten from API doesn't match the latest entry in ArbDB")
}

// Test that LRU caching works
builder.execConfig.BlockMetadataApiCacheSize = 10
builder.RestartL2Node(t)
l2rpc = builder.L2.Stack.Attach()
err = l2rpc.CallContext(ctx, &result, "arb_getRawBlockMetadata", hexutil.Uint64(start), hexutil.Uint64(end))
Require(t, err)

arbDb = builder.L2.ConsensusNode.ArbDB
updatedBlockMetadata := []byte{2, 12}
arbDb.Put(dbKey([]byte("t"), 1), updatedBlockMetadata)

err = l2rpc.CallContext(ctx, &result, "arb_getRawBlockMetadata", hexutil.Uint64(1), hexutil.Uint64(1))
Require(t, err)
if len(result) != 1 {
t.Fatal("result returned with more than one entry")
}
if bytes.Equal(updatedBlockMetadata, result[0].RawMetadata) {
t.Fatal("BlockMetadata should've been fetched from cache and not the db")
}
if !bytes.Equal(sampleBulkData[0].RawMetadata, result[0].RawMetadata) {
t.Fatal("incorrect caching of BlockMetadata")
}
}

func TestSequencerFeed_ExpressLaneAuction_ExpressLaneTxsHaveAdvantage_TimeboostedFieldIsCorrect(t *testing.T) {
t.Parallel()

Expand Down
Loading