Skip to content

Commit 0f9369c

Browse files
authored
feat: add vote extension support (#1641)
<!-- Please read and fill out this form before submitting your PR. Please make sure you have reviewed our contributors guide before submitting your first PR. NOTE: PR titles should follow semantic commits: https://www.conventionalcommits.org/en/v1.0.0/ --> ## Overview Resolves #1593 <!-- Please provide an explanation of the PR, including the appropriate context, background, goal, and rationale. If there is an issue with this information, please provide a tl;dr and link the issue. Ex: Closes #<issue number> --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced voting extension capabilities in the blockchain management system, including saving and retrieving extended commit information. - Added a new test function to verify the functionality of extended commits in the blockchain store. - Introduced a new field in the state initialization to enable vote extensions at a specified block height. - **Enhancements** - Updated the block creation process to include extended commit information, improving the integration with voting extensions. - Streamlined the `SetHeight` method in the store for better performance and simplicity. - **Bug Fixes** - Adjusted test cases to accommodate new parameters related to extended commit information, ensuring consistency and reliability in block creation tests. - **Documentation** - Updated code comments and versioning information to reflect new functionalities and changes in mock functions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent cceb2d2 commit 0f9369c

File tree

9 files changed

+337
-29
lines changed

9 files changed

+337
-29
lines changed

block/manager.go

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,18 @@ import (
1010
"sync/atomic"
1111
"time"
1212

13-
goheaderstore "github.com/celestiaorg/go-header/store"
1413
abci "github.com/cometbft/cometbft/abci/types"
1514
cmcrypto "github.com/cometbft/cometbft/crypto"
1615
"github.com/cometbft/cometbft/crypto/merkle"
16+
cmproto "github.com/cometbft/cometbft/proto/tendermint/types"
1717
"github.com/cometbft/cometbft/proxy"
1818
cmtypes "github.com/cometbft/cometbft/types"
1919
ds "github.com/ipfs/go-datastore"
2020
"github.com/libp2p/go-libp2p/core/crypto"
2121
pkgErrors "github.com/pkg/errors"
2222

23+
goheaderstore "github.com/celestiaorg/go-header/store"
24+
2325
"github.com/rollkit/rollkit/config"
2426
"github.com/rollkit/rollkit/da"
2527
"github.com/rollkit/rollkit/mempool"
@@ -811,7 +813,11 @@ func (m *Manager) publishBlock(ctx context.Context) error {
811813
block = pendingBlock
812814
} else {
813815
m.logger.Info("Creating and publishing block", "height", newHeight)
814-
block, err = m.createBlock(newHeight, lastCommit, lastHeaderHash)
816+
extendedCommit, err := m.getExtendedCommit(ctx, height)
817+
if err != nil {
818+
return fmt.Errorf("failed to load extended commit for height %d: %w", height, err)
819+
}
820+
block, err = m.createBlock(newHeight, lastCommit, lastHeaderHash, extendedCommit)
815821
if err != nil {
816822
return err
817823
}
@@ -863,6 +869,10 @@ func (m *Manager) publishBlock(ctx context.Context) error {
863869
return err
864870
}
865871

872+
if err := m.processVoteExtension(ctx, block, newHeight); err != nil {
873+
return err
874+
}
875+
866876
// set the commit to current block's signed header
867877
block.SignedHeader.Commit = *commit
868878
// Validate the created block before storing
@@ -926,6 +936,60 @@ func (m *Manager) publishBlock(ctx context.Context) error {
926936
return nil
927937
}
928938

939+
func (m *Manager) processVoteExtension(ctx context.Context, block *types.Block, newHeight uint64) error {
940+
if !m.voteExtensionEnabled(newHeight) {
941+
return nil
942+
}
943+
944+
extension, err := m.executor.ExtendVote(ctx, block)
945+
if err != nil {
946+
return fmt.Errorf("error returned by ExtendVote: %w", err)
947+
}
948+
sign, err := m.proposerKey.Sign(extension)
949+
if err != nil {
950+
return fmt.Errorf("error signing vote extension: %w", err)
951+
}
952+
extendedCommit := buildExtendedCommit(block, extension, sign)
953+
err = m.store.SaveExtendedCommit(ctx, newHeight, extendedCommit)
954+
if err != nil {
955+
return fmt.Errorf("failed to save extended commit: %w", err)
956+
}
957+
return nil
958+
}
959+
960+
func (m *Manager) voteExtensionEnabled(newHeight uint64) bool {
961+
enableHeight := m.lastState.ConsensusParams.Abci.VoteExtensionsEnableHeight
962+
return m.lastState.ConsensusParams.Abci != nil && enableHeight != 0 && uint64(enableHeight) <= newHeight
963+
}
964+
965+
func (m *Manager) getExtendedCommit(ctx context.Context, height uint64) (abci.ExtendedCommitInfo, error) {
966+
emptyExtendedCommit := abci.ExtendedCommitInfo{}
967+
if !m.voteExtensionEnabled(height) || height <= uint64(m.genesis.InitialHeight) {
968+
return emptyExtendedCommit, nil
969+
}
970+
extendedCommit, err := m.store.GetExtendedCommit(ctx, height)
971+
if err != nil {
972+
return emptyExtendedCommit, err
973+
}
974+
return *extendedCommit, nil
975+
}
976+
977+
func buildExtendedCommit(block *types.Block, extension []byte, sign []byte) *abci.ExtendedCommitInfo {
978+
extendedCommit := &abci.ExtendedCommitInfo{
979+
Round: 0,
980+
Votes: []abci.ExtendedVoteInfo{{
981+
Validator: abci.Validator{
982+
Address: block.SignedHeader.Validators.GetProposer().Address,
983+
Power: block.SignedHeader.Validators.GetProposer().VotingPower,
984+
},
985+
VoteExtension: extension,
986+
ExtensionSignature: sign,
987+
BlockIdFlag: cmproto.BlockIDFlagCommit,
988+
}},
989+
}
990+
return extendedCommit
991+
}
992+
929993
func (m *Manager) recordMetrics(block *types.Block) {
930994
m.metrics.NumTxs.Set(float64(len(block.Data.Txs)))
931995
m.metrics.TotalTxs.Add(float64(len(block.Data.Txs)))
@@ -1057,10 +1121,10 @@ func (m *Manager) getLastBlockTime() time.Time {
10571121
return m.lastState.LastBlockTime
10581122
}
10591123

1060-
func (m *Manager) createBlock(height uint64, lastCommit *types.Commit, lastHeaderHash types.Hash) (*types.Block, error) {
1124+
func (m *Manager) createBlock(height uint64, lastCommit *types.Commit, lastHeaderHash types.Hash, extendedCommit abci.ExtendedCommitInfo) (*types.Block, error) {
10611125
m.lastStateMtx.RLock()
10621126
defer m.lastStateMtx.RUnlock()
1063-
return m.executor.CreateBlock(height, lastCommit, lastHeaderHash, m.lastState)
1127+
return m.executor.CreateBlock(height, lastCommit, extendedCommit, lastHeaderHash, m.lastState)
10641128
}
10651129

10661130
func (m *Manager) applyBlock(ctx context.Context, block *types.Block) (types.State, *abci.ResponseFinalizeBlock, error) {

node/full_node_test.go

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,27 @@ import (
1111
"testing"
1212
"time"
1313

14+
abci "github.com/cometbft/cometbft/abci/types"
1415
cmconfig "github.com/cometbft/cometbft/config"
16+
cmproto "github.com/cometbft/cometbft/proto/tendermint/types"
1517
"github.com/cometbft/cometbft/proxy"
16-
17-
goDA "github.com/rollkit/go-da"
18-
"github.com/rollkit/rollkit/config"
19-
test "github.com/rollkit/rollkit/test/log"
18+
cmtypes "github.com/cometbft/cometbft/types"
2019

2120
"github.com/stretchr/testify/assert"
2221
"github.com/stretchr/testify/mock"
2322
"github.com/stretchr/testify/require"
2423

25-
abci "github.com/cometbft/cometbft/abci/types"
2624
"github.com/libp2p/go-libp2p/core/crypto"
2725
"github.com/libp2p/go-libp2p/core/peer"
2826

2927
testutils "github.com/celestiaorg/utils/test"
3028

29+
goDA "github.com/rollkit/go-da"
3130
"github.com/rollkit/rollkit/block"
31+
"github.com/rollkit/rollkit/config"
3232
"github.com/rollkit/rollkit/da"
3333
"github.com/rollkit/rollkit/mempool"
34+
test "github.com/rollkit/rollkit/test/log"
3435
"github.com/rollkit/rollkit/test/mocks"
3536
"github.com/rollkit/rollkit/types"
3637
)
@@ -262,6 +263,63 @@ func TestPendingBlocks(t *testing.T) {
262263

263264
}
264265

266+
func TestVoteExtension(t *testing.T) {
267+
require := require.New(t)
268+
269+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
270+
defer cancel()
271+
272+
const voteExtensionEnableHeight = 5
273+
const expectedExtension = "vote extension from height %d"
274+
275+
// Create & configure node with app. Get signing key for mock functions.
276+
app := &mocks.Application{}
277+
app.On("InitChain", mock.Anything, mock.Anything).Return(&abci.ResponseInitChain{}, nil)
278+
node, signingKey := createAggregatorWithApp(ctx, app, voteExtensionEnableHeight, t)
279+
require.NotNil(node)
280+
require.NotNil(signingKey)
281+
282+
prepareProposalVoteExtChecker := func(_ context.Context, req *abci.RequestPrepareProposal) (*abci.ResponsePrepareProposal, error) {
283+
if req.Height <= voteExtensionEnableHeight {
284+
require.Empty(req.LocalLastCommit.Votes)
285+
} else {
286+
require.Len(req.LocalLastCommit.Votes, 1)
287+
extendedCommit := req.LocalLastCommit.Votes[0]
288+
require.NotNil(extendedCommit)
289+
require.Equal(extendedCommit.BlockIdFlag, cmproto.BlockIDFlagCommit)
290+
// during PrepareProposal at height h, vote extensions from previous block (h-1) is available
291+
require.Equal([]byte(fmt.Sprintf(expectedExtension, req.Height-1)), extendedCommit.VoteExtension)
292+
require.NotNil(extendedCommit.Validator)
293+
require.NotNil(extendedCommit.Validator.Address)
294+
require.NotEmpty(extendedCommit.ExtensionSignature)
295+
ok, err := signingKey.GetPublic().Verify(extendedCommit.VoteExtension, extendedCommit.ExtensionSignature)
296+
require.NoError(err)
297+
require.True(ok)
298+
}
299+
return &abci.ResponsePrepareProposal{
300+
Txs: req.Txs,
301+
}, nil
302+
}
303+
304+
voteExtension := func(_ context.Context, req *abci.RequestExtendVote) (*abci.ResponseExtendVote, error) {
305+
return &abci.ResponseExtendVote{
306+
VoteExtension: []byte(fmt.Sprintf(expectedExtension, req.Height)),
307+
}, nil
308+
}
309+
310+
app.On("Commit", mock.Anything, mock.Anything).Return(&abci.ResponseCommit{}, nil)
311+
app.On("PrepareProposal", mock.Anything, mock.Anything).Return(prepareProposalVoteExtChecker)
312+
app.On("ProcessProposal", mock.Anything, mock.Anything).Return(&abci.ResponseProcessProposal{Status: abci.ResponseProcessProposal_ACCEPT}, nil)
313+
app.On("FinalizeBlock", mock.Anything, mock.Anything).Return(finalizeBlockResponse)
314+
app.On("ExtendVote", mock.Anything, mock.Anything).Return(voteExtension)
315+
require.NotNil(app)
316+
317+
require.NoError(node.Start())
318+
require.NoError(waitForAtLeastNBlocks(node, 10, Store))
319+
require.NoError(node.Stop())
320+
app.AssertExpectations(t)
321+
}
322+
265323
func createAggregatorWithPersistence(ctx context.Context, dbPath string, dalc *da.DAClient, t *testing.T) (Node, *mocks.Application) {
266324
t.Helper()
267325

@@ -302,6 +360,46 @@ func createAggregatorWithPersistence(ctx context.Context, dbPath string, dalc *d
302360
return fullNode, app
303361
}
304362

363+
func createAggregatorWithApp(ctx context.Context, app abci.Application, voteExtensionEnableHeight int64, t *testing.T) (Node, crypto.PrivKey) {
364+
t.Helper()
365+
366+
key, _, _ := crypto.GenerateEd25519Key(rand.Reader)
367+
genesis, genesisValidatorKey := types.GetGenesisWithPrivkey()
368+
genesis.ConsensusParams = &cmtypes.ConsensusParams{
369+
Block: cmtypes.DefaultBlockParams(),
370+
Evidence: cmtypes.DefaultEvidenceParams(),
371+
Validator: cmtypes.DefaultValidatorParams(),
372+
Version: cmtypes.DefaultVersionParams(),
373+
ABCI: cmtypes.ABCIParams{VoteExtensionsEnableHeight: voteExtensionEnableHeight},
374+
}
375+
signingKey, err := types.PrivKeyToSigningKey(genesisValidatorKey)
376+
require.NoError(t, err)
377+
378+
node, err := NewNode(
379+
ctx,
380+
config.NodeConfig{
381+
DAAddress: MockDAAddress,
382+
DANamespace: MockDANamespace,
383+
Aggregator: true,
384+
BlockManagerConfig: config.BlockManagerConfig{
385+
BlockTime: 100 * time.Millisecond,
386+
DABlockTime: 300 * time.Millisecond,
387+
},
388+
Light: false,
389+
},
390+
key,
391+
signingKey,
392+
proxy.NewLocalClientCreator(app),
393+
genesis,
394+
DefaultMetricsProvider(cmconfig.DefaultInstrumentationConfig()),
395+
test.NewFileLoggerCustom(t, test.TempLogFileName(t, "")),
396+
)
397+
require.NoError(t, err)
398+
require.NotNil(t, node)
399+
400+
return node, signingKey
401+
}
402+
305403
// setupMockApplication initializes a mock application
306404
func setupMockApplication() *mocks.Application {
307405
app := &mocks.Application{}

state/executor.go

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (e *BlockExecutor) InitChain(genesis *cmtypes.GenesisDoc) (*abci.ResponseIn
9292
}
9393

9494
// CreateBlock reaps transactions from mempool and builds a block.
95-
func (e *BlockExecutor) CreateBlock(height uint64, lastCommit *types.Commit, lastHeaderHash types.Hash, state types.State) (*types.Block, error) {
95+
func (e *BlockExecutor) CreateBlock(height uint64, lastCommit *types.Commit, lastExtendedCommit abci.ExtendedCommitInfo, lastHeaderHash types.Hash, state types.State) (*types.Block, error) {
9696
maxBytes := state.ConsensusParams.Block.MaxBytes
9797
emptyMaxBytes := maxBytes == -1
9898
if emptyMaxBytes {
@@ -140,12 +140,9 @@ func (e *BlockExecutor) CreateBlock(height uint64, lastCommit *types.Commit, las
140140
rpp, err := e.proxyApp.PrepareProposal(
141141
context.TODO(),
142142
&abci.RequestPrepareProposal{
143-
MaxTxBytes: maxBytes,
144-
Txs: mempoolTxs.ToSliceOfBytes(),
145-
LocalLastCommit: abci.ExtendedCommitInfo{
146-
Round: 0,
147-
Votes: []abci.ExtendedVoteInfo{},
148-
},
143+
MaxTxBytes: maxBytes,
144+
Txs: mempoolTxs.ToSliceOfBytes(),
145+
LocalLastCommit: lastExtendedCommit,
149146
Misbehavior: []abci.Misbehavior{},
150147
Height: int64(block.Height()),
151148
Time: block.Time(),
@@ -241,6 +238,32 @@ func (e *BlockExecutor) ApplyBlock(ctx context.Context, state types.State, block
241238
return state, resp, nil
242239
}
243240

241+
// ExtendVote calls the ExtendVote ABCI method on the proxy app.
242+
func (e *BlockExecutor) ExtendVote(ctx context.Context, block *types.Block) ([]byte, error) {
243+
resp, err := e.proxyApp.ExtendVote(ctx, &abci.RequestExtendVote{
244+
Hash: block.Hash(),
245+
Height: int64(block.Height()),
246+
Time: block.Time(),
247+
Txs: block.Data.Txs.ToSliceOfBytes(),
248+
ProposedLastCommit: abci.CommitInfo{
249+
Votes: []abci.VoteInfo{{
250+
Validator: abci.Validator{
251+
Address: block.SignedHeader.Validators.GetProposer().Address,
252+
Power: block.SignedHeader.Validators.GetProposer().VotingPower,
253+
},
254+
BlockIdFlag: cmproto.BlockIDFlagCommit,
255+
}},
256+
},
257+
Misbehavior: nil,
258+
NextValidatorsHash: block.SignedHeader.ValidatorHash,
259+
ProposerAddress: block.SignedHeader.ProposerAddress,
260+
})
261+
if err != nil {
262+
return nil, err
263+
}
264+
return resp.VoteExtension, nil
265+
}
266+
244267
// Commit commits the block
245268
func (e *BlockExecutor) Commit(ctx context.Context, state types.State, block *types.Block, resp *abci.ResponseFinalizeBlock) ([]byte, uint64, error) {
246269
appHash, retainHeight, err := e.commit(ctx, state, block, resp)

state/executor_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func doTestCreateBlock(t *testing.T) {
6060
state.ConsensusParams.Block.MaxGas = 100000
6161

6262
// empty block
63-
block, err := executor.CreateBlock(1, &types.Commit{}, []byte{}, state)
63+
block, err := executor.CreateBlock(1, &types.Commit{}, abci.ExtendedCommitInfo{}, []byte{}, state)
6464
require.NoError(err)
6565
require.NotNil(block)
6666
assert.Empty(block.Data.Txs)
@@ -69,7 +69,7 @@ func doTestCreateBlock(t *testing.T) {
6969
// one small Tx
7070
err = mpool.CheckTx([]byte{1, 2, 3, 4}, func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{})
7171
require.NoError(err)
72-
block, err = executor.CreateBlock(2, &types.Commit{}, []byte{}, state)
72+
block, err = executor.CreateBlock(2, &types.Commit{}, abci.ExtendedCommitInfo{}, []byte{}, state)
7373
require.NoError(err)
7474
require.NotNil(block)
7575
assert.Equal(uint64(2), block.Height())
@@ -80,7 +80,7 @@ func doTestCreateBlock(t *testing.T) {
8080
require.NoError(err)
8181
err = mpool.CheckTx(make([]byte, 100), func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{})
8282
require.NoError(err)
83-
block, err = executor.CreateBlock(3, &types.Commit{}, []byte{}, state)
83+
block, err = executor.CreateBlock(3, &types.Commit{}, abci.ExtendedCommitInfo{}, []byte{}, state)
8484
require.NoError(err)
8585
require.NotNil(block)
8686
assert.Len(block.Data.Txs, 2)
@@ -90,7 +90,7 @@ func doTestCreateBlock(t *testing.T) {
9090
executor.maxBytes = 10
9191
err = mpool.CheckTx(make([]byte, 10), func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{})
9292
require.NoError(err)
93-
block, err = executor.CreateBlock(4, &types.Commit{}, []byte{}, state)
93+
block, err = executor.CreateBlock(4, &types.Commit{}, abci.ExtendedCommitInfo{}, []byte{}, state)
9494
require.NoError(err)
9595
require.NotNil(block)
9696
assert.Empty(block.Data.Txs)
@@ -171,7 +171,7 @@ func doTestApplyBlock(t *testing.T) {
171171

172172
err = mpool.CheckTx([]byte{1, 2, 3, 4}, func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{})
173173
require.NoError(err)
174-
block, err := executor.CreateBlock(1, &types.Commit{Signatures: []types.Signature{types.Signature([]byte{1, 1, 1})}}, []byte{}, state)
174+
block, err := executor.CreateBlock(1, &types.Commit{Signatures: []types.Signature{types.Signature([]byte{1, 1, 1})}}, abci.ExtendedCommitInfo{}, []byte{}, state)
175175
require.NoError(err)
176176
require.NotNil(block)
177177
assert.Equal(uint64(1), block.Height())
@@ -201,7 +201,7 @@ func doTestApplyBlock(t *testing.T) {
201201
require.NoError(mpool.CheckTx([]byte{5, 6, 7, 8, 9}, func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{}))
202202
require.NoError(mpool.CheckTx([]byte{1, 2, 3, 4, 5}, func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{}))
203203
require.NoError(mpool.CheckTx(make([]byte, 90), func(r *abci.ResponseCheckTx) {}, mempool.TxInfo{}))
204-
block, err = executor.CreateBlock(2, &types.Commit{Signatures: []types.Signature{types.Signature([]byte{1, 1, 1})}}, []byte{}, newState)
204+
block, err = executor.CreateBlock(2, &types.Commit{Signatures: []types.Signature{types.Signature([]byte{1, 1, 1})}}, abci.ExtendedCommitInfo{}, []byte{}, newState)
205205
require.NoError(err)
206206
require.NotNil(block)
207207
assert.Equal(uint64(2), block.Height())

0 commit comments

Comments
 (0)