Skip to content

Commit e586987

Browse files
Copilotjulienrbrt
andcommitted
Add DA height polling functionality for genesis initialization
Co-authored-by: julienrbrt <29894366+julienrbrt@users.noreply.github.com>
1 parent 8cbca45 commit e586987

File tree

9 files changed

+387
-8
lines changed

9 files changed

+387
-8
lines changed

core/da/dummy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ func (d *DummyDA) StopHeightTicker() {
7777
close(d.stopCh)
7878
}
7979

80+
// GetCurrentHeight returns the current DA height
81+
func (d *DummyDA) GetCurrentHeight() uint64 {
82+
d.mu.RLock()
83+
defer d.mu.RUnlock()
84+
return d.currentHeight
85+
}
86+
8087
// GasPrice returns the gas price for the DA layer.
8188
func (d *DummyDA) GasPrice(ctx context.Context) (float64, error) {
8289
return d.gasPrice, nil

node/full.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
coreexecutor "github.com/evstack/ev-node/core/execution"
2323
coresequencer "github.com/evstack/ev-node/core/sequencer"
2424
"github.com/evstack/ev-node/pkg/config"
25+
"github.com/evstack/ev-node/pkg/da_client"
2526
genesispkg "github.com/evstack/ev-node/pkg/genesis"
2627
"github.com/evstack/ev-node/pkg/p2p"
2728
rpcserver "github.com/evstack/ev-node/pkg/rpc/server"
@@ -199,6 +200,37 @@ func initBlockManager(
199200
) (*block.Manager, error) {
200201
logger.Debug().Bytes("address", genesis.ProposerAddress).Msg("Proposer address")
201202

203+
// Handle DA height polling for genesis initialization when DA start time is zero
204+
// and node is not an aggregator
205+
if genesis.GenesisDAStartTime.IsZero() && !nodeConfig.Node.Aggregator {
206+
if nodeConfig.DA.AggregatorEndpoint != "" {
207+
logger.Info().Str("aggregator_endpoint", nodeConfig.DA.AggregatorEndpoint).Msg("Genesis DA start time is zero, polling aggregator for DA height")
208+
209+
// Create DA client to poll aggregator endpoint
210+
daClient := da_client.NewClient()
211+
212+
// Poll with a reasonable timeout and interval
213+
pollCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
214+
defer cancel()
215+
216+
pollInterval := 5 * time.Second
217+
daHeight, err := daClient.PollDAHeight(pollCtx, nodeConfig.DA.AggregatorEndpoint, pollInterval)
218+
if err != nil {
219+
return nil, fmt.Errorf("failed to poll DA height from aggregator: %w", err)
220+
}
221+
222+
logger.Info().Uint64("da_height", daHeight).Msg("Successfully polled DA height from aggregator")
223+
224+
// Update genesis with current time (the actual DA height will be used via DA.StartHeight config)
225+
// We set the time to now since the exact time doesn't matter for initialization
226+
genesis.GenesisDAStartTime = time.Now()
227+
228+
} else {
229+
logger.Warn().Msg("Genesis DA start time is zero but no aggregator endpoint configured - using current time")
230+
genesis.GenesisDAStartTime = time.Now()
231+
}
232+
}
233+
202234
blockManager, err := block.NewManager(
203235
ctx,
204236
signer,

pkg/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const (
7070
FlagDAMempoolTTL = FlagPrefixEvnode + "da.mempool_ttl"
7171
// FlagDAMaxSubmitAttempts is a flag for specifying the maximum DA submit attempts
7272
FlagDAMaxSubmitAttempts = FlagPrefixEvnode + "da.max_submit_attempts"
73+
// FlagDAAggregatorEndpoint is a flag for specifying the aggregator endpoint to poll for DA height
74+
FlagDAAggregatorEndpoint = FlagPrefixEvnode + "da.aggregator_endpoint"
7375

7476
// P2P configuration flags
7577

@@ -164,6 +166,7 @@ type DAConfig struct {
164166
StartHeight uint64 `mapstructure:"start_height" yaml:"start_height" comment:"Starting block height on the DA layer from which to begin syncing. Useful when deploying a new chain on an existing DA chain."`
165167
MempoolTTL uint64 `mapstructure:"mempool_ttl" yaml:"mempool_ttl" comment:"Number of DA blocks after which a transaction is considered expired and dropped from the mempool. Controls retry backoff timing."`
166168
MaxSubmitAttempts int `mapstructure:"max_submit_attempts" yaml:"max_submit_attempts" comment:"Maximum number of attempts to submit data to the DA layer before giving up. Higher values provide more resilience but can delay error reporting."`
169+
AggregatorEndpoint string `mapstructure:"aggregator_endpoint" yaml:"aggregator_endpoint" comment:"HTTP endpoint of an aggregator node to poll for DA height during genesis initialization (only used by non-aggregator nodes when genesis DA start time is zero)."`
167170
}
168171

169172
// GetHeaderNamespace returns the namespace for header submissions, falling back to the legacy namespace if not set

pkg/config/defaults.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ var DefaultConfig = Config{
5252
TrustedHash: "",
5353
},
5454
DA: DAConfig{
55-
Address: "http://localhost:7980",
56-
BlockTime: DurationWrapper{6 * time.Second},
57-
GasPrice: -1,
58-
GasMultiplier: 0,
59-
MaxSubmitAttempts: 30,
60-
Namespace: "",
61-
HeaderNamespace: "rollkit-headers",
62-
DataNamespace: "rollkit-data",
55+
Address: "http://localhost:7980",
56+
BlockTime: DurationWrapper{6 * time.Second},
57+
GasPrice: -1,
58+
GasMultiplier: 0,
59+
MaxSubmitAttempts: 30,
60+
Namespace: "",
61+
HeaderNamespace: "rollkit-headers",
62+
DataNamespace: "rollkit-data",
63+
AggregatorEndpoint: "",
6364
},
6465
Instrumentation: DefaultInstrumentationConfig(),
6566
Log: LogConfig{

pkg/da_client/client.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package da_client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"net/http"
8+
"time"
9+
)
10+
11+
// HeightResponse represents the response from the /da/height endpoint
12+
type HeightResponse struct {
13+
Height uint64 `json:"height"`
14+
Timestamp time.Time `json:"timestamp"`
15+
}
16+
17+
// Client is a simple HTTP client for polling DA height from aggregator endpoints
18+
type Client struct {
19+
httpClient *http.Client
20+
}
21+
22+
// NewClient creates a new DA client
23+
func NewClient() *Client {
24+
return &Client{
25+
httpClient: &http.Client{
26+
Timeout: 10 * time.Second,
27+
},
28+
}
29+
}
30+
31+
// GetDAHeight polls the given aggregator endpoint for the current DA height
32+
func (c *Client) GetDAHeight(ctx context.Context, aggregatorEndpoint string) (uint64, error) {
33+
if aggregatorEndpoint == "" {
34+
return 0, fmt.Errorf("aggregator endpoint is empty")
35+
}
36+
37+
url := fmt.Sprintf("%s/da/height", aggregatorEndpoint)
38+
39+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
40+
if err != nil {
41+
return 0, fmt.Errorf("failed to create request: %w", err)
42+
}
43+
44+
resp, err := c.httpClient.Do(req)
45+
if err != nil {
46+
return 0, fmt.Errorf("failed to make request to %s: %w", url, err)
47+
}
48+
defer resp.Body.Close()
49+
50+
if resp.StatusCode != http.StatusOK {
51+
return 0, fmt.Errorf("aggregator returned status %d from %s", resp.StatusCode, url)
52+
}
53+
54+
var heightResp HeightResponse
55+
if err := json.NewDecoder(resp.Body).Decode(&heightResp); err != nil {
56+
return 0, fmt.Errorf("failed to decode response: %w", err)
57+
}
58+
59+
return heightResp.Height, nil
60+
}
61+
62+
// PollDAHeight polls the aggregator endpoint until it gets a height > 0 or the context is cancelled
63+
func (c *Client) PollDAHeight(ctx context.Context, aggregatorEndpoint string, pollInterval time.Duration) (uint64, error) {
64+
ticker := time.NewTicker(pollInterval)
65+
defer ticker.Stop()
66+
67+
for {
68+
select {
69+
case <-ctx.Done():
70+
return 0, ctx.Err()
71+
case <-ticker.C:
72+
height, err := c.GetDAHeight(ctx, aggregatorEndpoint)
73+
if err != nil {
74+
// Log the error but continue polling
75+
continue
76+
}
77+
if height > 0 {
78+
return height, nil
79+
}
80+
// Height is 0, continue polling
81+
}
82+
}
83+
}

pkg/da_client/client_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package da_client
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
)
11+
12+
func TestClient_GetDAHeight(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
serverResponse HeightResponse
16+
serverStatus int
17+
expectError bool
18+
expectedHeight uint64
19+
}{
20+
{
21+
name: "successful response",
22+
serverResponse: HeightResponse{
23+
Height: 42,
24+
Timestamp: time.Now(),
25+
},
26+
serverStatus: http.StatusOK,
27+
expectError: false,
28+
expectedHeight: 42,
29+
},
30+
{
31+
name: "server error",
32+
serverStatus: http.StatusInternalServerError,
33+
expectError: true,
34+
},
35+
{
36+
name: "zero height",
37+
serverResponse: HeightResponse{
38+
Height: 0,
39+
Timestamp: time.Now(),
40+
},
41+
serverStatus: http.StatusOK,
42+
expectError: false,
43+
expectedHeight: 0,
44+
},
45+
}
46+
47+
for _, tt := range tests {
48+
t.Run(tt.name, func(t *testing.T) {
49+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
50+
if r.URL.Path != "/da/height" {
51+
http.Error(w, "not found", http.StatusNotFound)
52+
return
53+
}
54+
55+
if tt.serverStatus != http.StatusOK {
56+
http.Error(w, "server error", tt.serverStatus)
57+
return
58+
}
59+
60+
w.Header().Set("Content-Type", "application/json")
61+
json.NewEncoder(w).Encode(tt.serverResponse)
62+
}))
63+
defer server.Close()
64+
65+
client := NewClient()
66+
height, err := client.GetDAHeight(context.Background(), server.URL)
67+
68+
if tt.expectError {
69+
if err == nil {
70+
t.Errorf("expected error but got none")
71+
}
72+
return
73+
}
74+
75+
if err != nil {
76+
t.Errorf("unexpected error: %v", err)
77+
return
78+
}
79+
80+
if height != tt.expectedHeight {
81+
t.Errorf("expected height %d, got %d", tt.expectedHeight, height)
82+
}
83+
})
84+
}
85+
}
86+
87+
func TestClient_GetDAHeight_EmptyEndpoint(t *testing.T) {
88+
client := NewClient()
89+
_, err := client.GetDAHeight(context.Background(), "")
90+
if err == nil {
91+
t.Error("expected error for empty endpoint")
92+
}
93+
}
94+
95+
func TestClient_PollDAHeight(t *testing.T) {
96+
callCount := 0
97+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
98+
callCount++
99+
var height uint64
100+
if callCount == 1 {
101+
height = 0 // First call returns 0
102+
} else {
103+
height = 5 // Second call returns 5
104+
}
105+
106+
resp := HeightResponse{
107+
Height: height,
108+
Timestamp: time.Now(),
109+
}
110+
w.Header().Set("Content-Type", "application/json")
111+
json.NewEncoder(w).Encode(resp)
112+
}))
113+
defer server.Close()
114+
115+
client := NewClient()
116+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
117+
defer cancel()
118+
119+
height, err := client.PollDAHeight(ctx, server.URL, 100*time.Millisecond)
120+
if err != nil {
121+
t.Errorf("unexpected error: %v", err)
122+
}
123+
124+
if height != 5 {
125+
t.Errorf("expected height 5, got %d", height)
126+
}
127+
128+
if callCount < 2 {
129+
t.Errorf("expected at least 2 calls, got %d", callCount)
130+
}
131+
}

0 commit comments

Comments
 (0)