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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add disaster recovery for sequencer
- Catch up possible DA-only blocks when restarting. [#3057](https://github.com/evstack/ev-node/pull/3057)
- Verify DA and P2P state on restart (prevent double-signing). [#3061](https://github.com/evstack/ev-node/pull/3061)

## v1.0.0-rc.4

### Changes
Expand Down
8 changes: 4 additions & 4 deletions apps/evm/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ module github.com/evstack/ev-node/apps/evm

go 1.25.6

//replace (
// github.com/evstack/ev-node => ../../
// github.com/evstack/ev-node/execution/evm => ../../execution/evm
//)
replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/execution/evm => ../../execution/evm
)

require (
github.com/ethereum/go-ethereum v1.16.8
Expand Down
4 changes: 0 additions & 4 deletions apps/evm/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,8 @@ github.com/ethereum/go-ethereum v1.16.8 h1:LLLfkZWijhR5m6yrAXbdlTeXoqontH+Ga2f9i
github.com/ethereum/go-ethereum v1.16.8/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/evstack/ev-node v1.0.0-rc.4 h1:Ju7pSETFdadBZxmAj0//4z7hHkXbSRDy9iTzhF60Dew=
github.com/evstack/ev-node v1.0.0-rc.4/go.mod h1:xGCH5NCdGiYk6v3GVPm4NhzAtcKQgnaVnORg8b4tbOk=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/evstack/ev-node/execution/evm v1.0.0-rc.3 h1:3o8H1TNywnst56lo2RlS2SXulDfp9yZJtkYYh7ZJrdM=
github.com/evstack/ev-node/execution/evm v1.0.0-rc.3/go.mod h1:VUEEklKoclg45GL7dzLoDwu3UQ4ptT3rF8bw5zUmnRk=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
4 changes: 4 additions & 0 deletions apps/evm/server/force_inclusion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ func (m *mockDA) HasForcedInclusionNamespace() bool {
return true
}

func (m *mockDA) GetLatestDAHeight(_ context.Context) (uint64, error) {
return 0, nil
}

func TestForceInclusionServer_handleSendRawTransaction_Success(t *testing.T) {
testHeight := uint64(100)

Expand Down
8 changes: 4 additions & 4 deletions apps/grpc/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ module github.com/evstack/ev-node/apps/grpc

go 1.25.6

//replace (
// github.com/evstack/ev-node => ../../
// github.com/evstack/ev-node/execution/grpc => ../../execution/grpc
//)
replace (
github.com/evstack/ev-node => ../../
github.com/evstack/ev-node/execution/grpc => ../../execution/grpc
)

require (
github.com/evstack/ev-node v1.0.0-rc.4
Expand Down
4 changes: 0 additions & 4 deletions apps/grpc/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,8 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node v1.0.0-rc.4 h1:Ju7pSETFdadBZxmAj0//4z7hHkXbSRDy9iTzhF60Dew=
github.com/evstack/ev-node v1.0.0-rc.4/go.mod h1:xGCH5NCdGiYk6v3GVPm4NhzAtcKQgnaVnORg8b4tbOk=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/evstack/ev-node/execution/grpc v1.0.0-rc.1 h1:OzrWLDDY6/9+LWx0XmUqPzxs/CHZRJICOwQ0Me/i6dY=
github.com/evstack/ev-node/execution/grpc v1.0.0-rc.1/go.mod h1:Pr/sF6Zx8am9ZeWFcoz1jYPs0kXmf+OmL8Tz2Gyq7E4=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
Expand Down
2 changes: 1 addition & 1 deletion apps/testapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/evstack/ev-node/apps/testapp

go 1.25.6

//replace github.com/evstack/ev-node => ../../
replace github.com/evstack/ev-node => ../../

require (
github.com/evstack/ev-node v1.0.0-rc.4
Expand Down
2 changes: 0 additions & 2 deletions apps/testapp/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,6 @@ github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6Ni
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/envoyproxy/protoc-gen-validate v1.0.1/go.mod h1:0vj8bNkYbSTNS2PIyH87KZaeN4x9zpL9Qt8fQC7d+vs=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evstack/ev-node v1.0.0-rc.4 h1:Ju7pSETFdadBZxmAj0//4z7hHkXbSRDy9iTzhF60Dew=
github.com/evstack/ev-node v1.0.0-rc.4/go.mod h1:xGCH5NCdGiYk6v3GVPm4NhzAtcKQgnaVnORg8b4tbOk=
github.com/evstack/ev-node/core v1.0.0-rc.1 h1:Dic2PMUMAYUl5JW6DkDj6HXDEWYzorVJQuuUJOV0FjE=
github.com/evstack/ev-node/core v1.0.0-rc.1/go.mod h1:n2w/LhYQTPsi48m6lMj16YiIqsaQw6gxwjyJvR+B3sY=
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
Expand Down
17 changes: 17 additions & 0 deletions block/internal/da/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,23 @@ func (c *client) Retrieve(ctx context.Context, height uint64, namespace []byte)
}
}

// GetLatestDAHeight returns the latest height available on the DA layer by
// querying the network head.
func (c *client) GetLatestDAHeight(ctx context.Context) (uint64, error) {
headCtx, cancel := context.WithTimeout(ctx, c.defaultTimeout)
defer cancel()

header, err := c.headerAPI.NetworkHead(headCtx)
if err != nil {
return 0, fmt.Errorf("failed to get DA network head: %w", err)
}
if header == nil {
return 0, fmt.Errorf("DA network head returned nil header")
}

return header.Height, nil
}

// RetrieveForcedInclusion retrieves blobs from the forced inclusion namespace at the specified height.
func (c *client) RetrieveForcedInclusion(ctx context.Context, height uint64) datypes.ResultRetrieve {
if !c.hasForcedNamespace {
Expand Down
3 changes: 3 additions & 0 deletions block/internal/da/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ type Client interface {
// Get retrieves blobs by their IDs. Used for visualization and fetching specific blobs.
Get(ctx context.Context, ids []datypes.ID, namespace []byte) ([]datypes.Blob, error)

// GetLatestDAHeight returns the latest height available on the DA layer..
GetLatestDAHeight(ctx context.Context) (uint64, error)

// Namespace accessors.
GetHeaderNamespace() []byte
GetDataNamespace() []byte
Expand Down
14 changes: 14 additions & 0 deletions block/internal/da/tracing.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ func (t *tracedClient) Validate(ctx context.Context, ids []datypes.ID, proofs []
return res, nil
}

func (t *tracedClient) GetLatestDAHeight(ctx context.Context) (uint64, error) {
ctx, span := t.tracer.Start(ctx, "DA.GetLatestDAHeight")
defer span.End()

height, err := t.inner.GetLatestDAHeight(ctx)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
}
span.SetAttributes(attribute.Int64("da.latest_height", int64(height)))
return height, nil
}

func (t *tracedClient) GetHeaderNamespace() []byte { return t.inner.GetHeaderNamespace() }
func (t *tracedClient) GetDataNamespace() []byte { return t.inner.GetDataNamespace() }
func (t *tracedClient) GetForcedInclusionNamespace() []byte {
Expand Down
9 changes: 5 additions & 4 deletions block/internal/da/tracing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ func (m *mockFullClient) Validate(ctx context.Context, ids []datypes.ID, proofs
}
return nil, nil
}
func (m *mockFullClient) GetHeaderNamespace() []byte { return []byte{0x01} }
func (m *mockFullClient) GetDataNamespace() []byte { return []byte{0x02} }
func (m *mockFullClient) GetForcedInclusionNamespace() []byte { return []byte{0x03} }
func (m *mockFullClient) HasForcedInclusionNamespace() bool { return true }
func (m *mockFullClient) GetLatestDAHeight(_ context.Context) (uint64, error) { return 0, nil }
func (m *mockFullClient) GetHeaderNamespace() []byte { return []byte{0x01} }
func (m *mockFullClient) GetDataNamespace() []byte { return []byte{0x02} }
func (m *mockFullClient) GetForcedInclusionNamespace() []byte { return []byte{0x03} }
func (m *mockFullClient) HasForcedInclusionNamespace() bool { return true }

// setup a tracer provider + span recorder
func setupDATrace(t *testing.T, inner FullClient) (FullClient, *tracetest.SpanRecorder) {
Expand Down
44 changes: 42 additions & 2 deletions block/internal/syncing/syncer.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,9 +760,49 @@ func (s *Syncer) TrySyncNextBlock(ctx context.Context, event *common.DAHeightEve

// Update DA height if needed
// This height is only updated when a height is processed from DA as P2P
// events do not contain DA height information
// events do not contain DA height information.
//
// When a sequencer restarts after extended downtime, it produces "catch-up"
// blocks containing forced inclusion transactions from missed DA epochs and
// submits them to DA at the current (much higher) DA height. This creates a
// gap between the state's DAHeight (tracking forced inclusion epoch progress)
// and event.DaHeight (the DA submission height).
//
// If we jump state.DAHeight directly to event.DaHeight, subsequent calls to
// VerifyForcedInclusionTxs would check the wrong epoch (the submission epoch
// instead of the next forced-inclusion epoch), causing valid catch-up blocks
// to be incorrectly flagged as malicious.
//
// To handle this, when the gap exceeds one DA epoch, we advance DAHeight by
// exactly one epoch per block. This lets the forced inclusion verifier check
// the correct epoch for each catch-up block. Once the sequencer finishes
// catching up and the gap closes, DAHeight converges to event.DaHeight.
if event.DaHeight > newState.DAHeight {
newState.DAHeight = event.DaHeight
epochSize := s.genesis.DAEpochForcedInclusion
gap := event.DaHeight - newState.DAHeight

if epochSize > 0 && gap > epochSize {
// Large gap detected — likely catch-up blocks from a restarted sequencer.
// Advance DAHeight by one epoch to keep forced inclusion verification
// aligned with the epoch the sequencer is replaying.
_, epochEnd, _ := types.CalculateEpochBoundaries(
newState.DAHeight, s.genesis.DAStartHeight, epochSize,
)
nextEpochStart := epochEnd + 1
if nextEpochStart > event.DaHeight {
// Shouldn't happen, but clamp to event.DaHeight as a safety net.
nextEpochStart = event.DaHeight
}
s.logger.Debug().
Uint64("current_da_height", newState.DAHeight).
Uint64("event_da_height", event.DaHeight).
Uint64("advancing_to", nextEpochStart).
Uint64("gap", gap).
Msg("large DA height gap detected (sequencer catch-up), advancing DA height by one epoch")
newState.DAHeight = nextEpochStart
} else {
newState.DAHeight = event.DaHeight
}
}

batch, err := s.store.NewBatch(ctx)
Expand Down
Loading
Loading