Skip to content

Commit da7b970

Browse files
committed
add key rotation
1 parent 3efcf93 commit da7b970

20 files changed

Lines changed: 956 additions & 39 deletions

block/internal/executing/executor.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func NewExecutor(
126126
return nil, fmt.Errorf("failed to get address: %w", err)
127127
}
128128

129-
if !bytes.Equal(addr, genesis.ProposerAddress) {
129+
if !genesis.HasScheduledProposer(addr) {
130130
return nil, common.ErrNotProposer
131131
}
132132
}
@@ -696,6 +696,10 @@ func (e *Executor) RetrieveBatch(ctx context.Context) (*BatchData, error) {
696696
func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *BatchData) (*types.SignedHeader, *types.Data, error) {
697697
currentState := e.getLastState()
698698
headerTime := uint64(e.genesis.StartTime.UnixNano())
699+
proposer, err := e.genesis.ProposerAtHeight(height)
700+
if err != nil {
701+
return nil, nil, fmt.Errorf("resolve proposer for height %d: %w", height, err)
702+
}
699703

700704
var lastHeaderHash types.Hash
701705
var lastDataHash types.Hash
@@ -728,22 +732,30 @@ func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *Ba
728732

729733
// Get signer info and validator hash
730734
var pubKey crypto.PubKey
735+
var signerAddress []byte
731736
var validatorHash types.Hash
732737

733738
if e.signer != nil {
734-
var err error
735739
pubKey, err = e.signer.GetPublic()
736740
if err != nil {
737741
return nil, nil, fmt.Errorf("failed to get public key: %w", err)
738742
}
739743

740-
validatorHash, err = e.options.ValidatorHasherProvider(e.genesis.ProposerAddress, pubKey)
744+
signerAddress, err = e.signer.GetAddress()
745+
if err != nil {
746+
return nil, nil, fmt.Errorf("failed to get signer address: %w", err)
747+
}
748+
749+
if err := e.genesis.ValidateProposer(height, signerAddress, pubKey); err != nil {
750+
return nil, nil, fmt.Errorf("signer does not match proposer schedule: %w", err)
751+
}
752+
753+
validatorHash, err = e.options.ValidatorHasherProvider(proposer.Address, pubKey)
741754
if err != nil {
742755
return nil, nil, fmt.Errorf("failed to get validator hash: %w", err)
743756
}
744757
} else {
745-
var err error
746-
validatorHash, err = e.options.ValidatorHasherProvider(e.genesis.ProposerAddress, nil)
758+
validatorHash, err = e.options.ValidatorHasherProvider(proposer.Address, nil)
747759
if err != nil {
748760
return nil, nil, fmt.Errorf("failed to get validator hash: %w", err)
749761
}
@@ -763,13 +775,13 @@ func (e *Executor) CreateBlock(ctx context.Context, height uint64, batchData *Ba
763775
},
764776
LastHeaderHash: lastHeaderHash,
765777
AppHash: currentState.AppHash,
766-
ProposerAddress: e.genesis.ProposerAddress,
778+
ProposerAddress: proposer.Address,
767779
ValidatorHash: validatorHash,
768780
},
769781
Signature: lastSignature,
770782
Signer: types.Signer{
771783
PubKey: pubKey,
772-
Address: e.genesis.ProposerAddress,
784+
Address: proposer.Address,
773785
},
774786
}
775787

block/internal/executing/executor_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package executing
22

33
import (
4+
"context"
45
"testing"
56
"time"
67

@@ -12,6 +13,7 @@ import (
1213

1314
"github.com/evstack/ev-node/block/internal/cache"
1415
"github.com/evstack/ev-node/block/internal/common"
16+
coreseq "github.com/evstack/ev-node/core/sequencer"
1517
"github.com/evstack/ev-node/pkg/config"
1618
"github.com/evstack/ev-node/pkg/genesis"
1719
"github.com/evstack/ev-node/pkg/store"
@@ -121,3 +123,96 @@ func TestExecutor_NilBroadcasters(t *testing.T) {
121123
assert.Equal(t, cacheManager, executor.cache)
122124
assert.Equal(t, gen, executor.genesis)
123125
}
126+
127+
func TestExecutor_CreateBlock_UsesScheduledProposerForHeight(t *testing.T) {
128+
ds := sync.MutexWrap(datastore.NewMapDatastore())
129+
memStore := store.New(ds)
130+
131+
cacheManager, err := cache.NewManager(config.DefaultConfig(), memStore, zerolog.Nop())
132+
require.NoError(t, err)
133+
134+
metrics := common.NopMetrics()
135+
oldAddr, oldSignerInfo, _ := buildTestSigner(t)
136+
newAddr, newSignerInfo, newSigner := buildTestSigner(t)
137+
138+
entry1, err := genesis.NewProposerScheduleEntry(1, oldSignerInfo.PubKey)
139+
require.NoError(t, err)
140+
entry2, err := genesis.NewProposerScheduleEntry(2, newSignerInfo.PubKey)
141+
require.NoError(t, err)
142+
143+
gen := genesis.Genesis{
144+
ChainID: "test-chain",
145+
InitialHeight: 1,
146+
StartTime: time.Now().Add(-time.Second),
147+
ProposerAddress: entry1.Address,
148+
ProposerSchedule: []genesis.ProposerScheduleEntry{entry1, entry2},
149+
DAEpochForcedInclusion: 1,
150+
}
151+
152+
executor, err := NewExecutor(
153+
memStore,
154+
nil,
155+
nil,
156+
newSigner,
157+
cacheManager,
158+
metrics,
159+
config.DefaultConfig(),
160+
gen,
161+
nil,
162+
nil,
163+
zerolog.Nop(),
164+
common.DefaultBlockOptions(),
165+
make(chan error, 1),
166+
nil,
167+
)
168+
require.NoError(t, err)
169+
170+
prevHeader := &types.SignedHeader{
171+
Header: types.Header{
172+
Version: types.InitStateVersion,
173+
BaseHeader: types.BaseHeader{
174+
ChainID: gen.ChainID,
175+
Height: 1,
176+
Time: uint64(gen.StartTime.UnixNano()),
177+
},
178+
AppHash: []byte("state-root-0"),
179+
ProposerAddress: oldAddr,
180+
DataHash: common.DataHashForEmptyTxs,
181+
},
182+
Signature: types.Signature([]byte("sig-1")),
183+
Signer: oldSignerInfo,
184+
}
185+
prevData := &types.Data{
186+
Metadata: &types.Metadata{
187+
ChainID: gen.ChainID,
188+
Height: 1,
189+
Time: prevHeader.BaseHeader.Time,
190+
},
191+
Txs: nil,
192+
}
193+
194+
batch, err := memStore.NewBatch(context.Background())
195+
require.NoError(t, err)
196+
require.NoError(t, batch.SaveBlockData(prevHeader, prevData, &prevHeader.Signature))
197+
require.NoError(t, batch.SetHeight(1))
198+
require.NoError(t, batch.Commit())
199+
200+
executor.setLastState(types.State{
201+
Version: types.InitStateVersion,
202+
ChainID: gen.ChainID,
203+
InitialHeight: gen.InitialHeight,
204+
LastBlockHeight: 1,
205+
LastBlockTime: prevHeader.Time(),
206+
LastHeaderHash: prevHeader.Hash(),
207+
AppHash: []byte("state-root-1"),
208+
})
209+
210+
header, data, err := executor.CreateBlock(context.Background(), 2, &BatchData{
211+
Batch: &coreseq.Batch{},
212+
Time: time.Now(),
213+
})
214+
require.NoError(t, err)
215+
require.Equal(t, newAddr, header.ProposerAddress)
216+
require.Equal(t, newAddr, header.Signer.Address)
217+
require.Equal(t, uint64(2), data.Height())
218+
}

block/internal/submitting/da_submitter.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package submitting
22

33
import (
4-
"bytes"
54
"context"
65
"encoding/json"
76
"fmt"
@@ -476,10 +475,6 @@ func (s *DASubmitter) signData(ctx context.Context, unsignedDataList []*types.Si
476475
return nil, nil, fmt.Errorf("failed to get address: %w", err)
477476
}
478477

479-
if len(genesis.ProposerAddress) > 0 && !bytes.Equal(addr, genesis.ProposerAddress) {
480-
return nil, nil, fmt.Errorf("signer address mismatch with genesis proposer")
481-
}
482-
483478
signerInfo := types.Signer{
484479
PubKey: pubKey,
485480
Address: addr,
@@ -494,6 +489,10 @@ func (s *DASubmitter) signData(ctx context.Context, unsignedDataList []*types.Si
494489
continue
495490
}
496491

492+
if err := genesis.ValidateProposer(unsignedData.Height(), addr, pubKey); err != nil {
493+
return nil, nil, fmt.Errorf("signer does not match proposer schedule for data at height %d: %w", unsignedData.Height(), err)
494+
}
495+
497496
signature, err := signer.Sign(ctx, unsignedDataListBz[i])
498497
if err != nil {
499498
return nil, nil, fmt.Errorf("failed to sign data: %w", err)

block/internal/submitting/da_submitter_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,97 @@ func TestDASubmitter_SubmitData_Success(t *testing.T) {
343343
assert.True(t, ok)
344344
}
345345

346+
func TestDASubmitter_SubmitData_UsesScheduledProposerForHeight(t *testing.T) {
347+
submitter, st, cm, mockDA, gen := setupDASubmitterTest(t)
348+
ctx := context.Background()
349+
dataNamespace := datypes.NamespaceFromString(testDataNamespace).Bytes()
350+
351+
mockDA.On(
352+
"Submit",
353+
mock.Anything,
354+
mock.AnythingOfType("[][]uint8"),
355+
mock.AnythingOfType("float64"),
356+
dataNamespace,
357+
mock.Anything,
358+
).Return(func(_ context.Context, blobs [][]byte, _ float64, _ []byte, _ []byte) datypes.ResultSubmit {
359+
return datypes.ResultSubmit{BaseResult: datypes.BaseResult{Code: datypes.StatusSuccess, SubmittedCount: uint64(len(blobs)), Height: 2}}
360+
}).Once()
361+
362+
oldAddr, oldPub, _ := createTestSigner(t)
363+
nextAddr, nextPub, nextSigner := createTestSigner(t)
364+
365+
entry1, err := genesis.NewProposerScheduleEntry(gen.InitialHeight, oldPub)
366+
require.NoError(t, err)
367+
entry2, err := genesis.NewProposerScheduleEntry(2, nextPub)
368+
require.NoError(t, err)
369+
370+
gen.ProposerAddress = entry1.Address
371+
gen.ProposerSchedule = []genesis.ProposerScheduleEntry{entry1, entry2}
372+
submitter.genesis = gen
373+
374+
data1 := &types.Data{
375+
Metadata: &types.Metadata{
376+
ChainID: gen.ChainID,
377+
Height: 1,
378+
Time: uint64(time.Now().UnixNano()),
379+
},
380+
Txs: types.Txs{},
381+
}
382+
383+
header1 := &types.SignedHeader{
384+
Header: types.Header{
385+
BaseHeader: types.BaseHeader{
386+
ChainID: gen.ChainID,
387+
Height: 1,
388+
Time: uint64(time.Now().UnixNano()),
389+
},
390+
ProposerAddress: oldAddr,
391+
DataHash: common.DataHashForEmptyTxs,
392+
},
393+
Signer: types.Signer{PubKey: oldPub, Address: oldAddr},
394+
}
395+
396+
data := &types.Data{
397+
Metadata: &types.Metadata{
398+
ChainID: gen.ChainID,
399+
Height: 2,
400+
Time: uint64(time.Now().UnixNano()),
401+
},
402+
Txs: types.Txs{types.Tx("rotated-key-tx")},
403+
}
404+
405+
header := &types.SignedHeader{
406+
Header: types.Header{
407+
BaseHeader: types.BaseHeader{
408+
ChainID: gen.ChainID,
409+
Height: 2,
410+
Time: uint64(time.Now().UnixNano()),
411+
},
412+
ProposerAddress: nextAddr,
413+
DataHash: data.DACommitment(),
414+
},
415+
Signer: types.Signer{PubKey: nextPub, Address: nextAddr},
416+
}
417+
418+
sig1 := types.Signature([]byte("sig-1"))
419+
sig2 := types.Signature([]byte("sig-2"))
420+
batch, err := st.NewBatch(ctx)
421+
require.NoError(t, err)
422+
require.NoError(t, batch.SaveBlockData(header1, data1, &sig1))
423+
require.NoError(t, batch.SaveBlockData(header, data, &sig2))
424+
require.NoError(t, batch.SetHeight(2))
425+
require.NoError(t, batch.Commit())
426+
427+
signedDataList, marshalledData, err := cm.GetPendingData(ctx)
428+
require.NoError(t, err)
429+
err = submitter.SubmitData(ctx, signedDataList, marshalledData, cm, nextSigner, gen)
430+
require.NoError(t, err)
431+
432+
_, ok := cm.GetDataDAIncludedByHeight(2)
433+
assert.True(t, ok)
434+
assert.NotEqual(t, oldAddr, nextAddr)
435+
}
436+
346437
func TestDASubmitter_SubmitData_SkipsEmptyData(t *testing.T) {
347438
submitter, st, cm, mockDA, gen := setupDASubmitterTest(t)
348439
ctx := context.Background()

block/internal/syncing/assert.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package syncing
22

33
import (
4-
"bytes"
54
"errors"
65
"fmt"
76

87
"github.com/evstack/ev-node/pkg/genesis"
98
"github.com/evstack/ev-node/types"
109
)
1110

12-
func assertExpectedProposer(genesis genesis.Genesis, proposerAddr []byte) error {
13-
if !bytes.Equal(proposerAddr, genesis.ProposerAddress) {
14-
return fmt.Errorf("unexpected proposer: got %x, expected %x",
15-
proposerAddr, genesis.ProposerAddress)
11+
func assertExpectedProposer(genesis genesis.Genesis, height uint64, proposerAddr []byte, signer types.Signer) error {
12+
if err := genesis.ValidateProposer(height, proposerAddr, signer.PubKey); err != nil {
13+
return fmt.Errorf("unexpected proposer at height %d: %w", height, err)
1614
}
15+
1716
return nil
1817
}
1918

@@ -22,7 +21,7 @@ func assertValidSignedData(signedData *types.SignedData, genesis genesis.Genesis
2221
return errors.New("empty signed data")
2322
}
2423

25-
if err := assertExpectedProposer(genesis, signedData.Signer.Address); err != nil {
24+
if err := assertExpectedProposer(genesis, signedData.Height(), signedData.Signer.Address, signedData.Signer); err != nil {
2625
return err
2726
}
2827

block/internal/syncing/da_retriever.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ func (r *daRetriever) tryDecodeHeader(bz []byte, daHeight uint64) *types.SignedH
299299
return nil
300300
}
301301

302-
if err := r.assertExpectedProposer(header.ProposerAddress); err != nil {
302+
if err := r.assertExpectedProposer(header); err != nil {
303303
r.logger.Debug().Err(err).Msg("unexpected proposer")
304304
return nil
305305
}
@@ -355,9 +355,9 @@ func (r *daRetriever) tryDecodeData(bz []byte, daHeight uint64) *types.Data {
355355
return &signedData.Data
356356
}
357357

358-
// assertExpectedProposer validates the proposer address
359-
func (r *daRetriever) assertExpectedProposer(proposerAddr []byte) error {
360-
return assertExpectedProposer(r.genesis, proposerAddr)
358+
// assertExpectedProposer validates the proposer schedule entry for the header height.
359+
func (r *daRetriever) assertExpectedProposer(header *types.SignedHeader) error {
360+
return assertExpectedProposer(r.genesis, header.Height(), header.ProposerAddress, header.Signer)
361361
}
362362

363363
// assertValidSignedData validates signed data using the configured signature provider

block/internal/syncing/p2p_handler.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (h *P2PHandler) ProcessHeight(ctx context.Context, height uint64, heightInC
8181
}
8282
return err
8383
}
84-
if err := h.assertExpectedProposer(p2pHeader.ProposerAddress); err != nil {
84+
if err := h.assertExpectedProposer(p2pHeader.SignedHeader); err != nil {
8585
h.logger.Debug().Uint64("height", height).Err(err).Msg("invalid header from P2P")
8686
return err
8787
}
@@ -125,11 +125,11 @@ func (h *P2PHandler) ProcessHeight(ctx context.Context, height uint64, heightInC
125125
return nil
126126
}
127127

128-
// assertExpectedProposer validates the proposer address.
129-
func (h *P2PHandler) assertExpectedProposer(proposerAddr []byte) error {
130-
if !bytes.Equal(h.genesis.ProposerAddress, proposerAddr) {
131-
return fmt.Errorf("proposer address mismatch: got %x, expected %x",
132-
proposerAddr, h.genesis.ProposerAddress)
128+
// assertExpectedProposer validates the proposer schedule entry for the header height.
129+
func (h *P2PHandler) assertExpectedProposer(header *types.SignedHeader) error {
130+
if err := assertExpectedProposer(h.genesis, header.Height(), header.ProposerAddress, header.Signer); err != nil {
131+
return err
133132
}
133+
134134
return nil
135135
}

0 commit comments

Comments
 (0)