From 0a245956654cb7a8b12f989706e70913fac1b2b7 Mon Sep 17 00:00:00 2001 From: Tural Devrishev Date: Fri, 11 Sep 2026 10:23:16 +0300 Subject: [PATCH 1/4] config: fix transactionHashes parameter naming Signed-off-by: Tural Devrishev --- config.go | 2 +- internal/consensus/constructors.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 11aedb639..d12644157 100644 --- a/config.go +++ b/config.go @@ -388,7 +388,7 @@ func WithNewConsensusPayload[H Hash](f func(ctx *Context[H], typ MessageType, ms } // WithNewPrepareRequest sets NewPrepareRequest. -func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionsHashes []H) PrepareRequest[H]) func(config *Config[H]) { +func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H]) func(config *Config[H]) { return func(cfg *Config[H]) { cfg.NewPrepareRequest = f } diff --git a/internal/consensus/constructors.go b/internal/consensus/constructors.go index 096fa37d6..9a0636b96 100644 --- a/internal/consensus/constructors.go +++ b/internal/consensus/constructors.go @@ -21,9 +21,9 @@ func NewConsensusPayload(t dbft.MessageType, height uint32, validatorIndex uint1 } // NewPrepareRequest returns minimal prepareRequest implementation. -func NewPrepareRequest(ts uint64, nonce uint64, transactionsHashes []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { +func NewPrepareRequest(ts uint64, nonce uint64, transactionHashes []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { return &prepareRequest{ - transactionHashes: transactionsHashes, + transactionHashes: transactionHashes, nonce: nonce, timestamp: nanoSecToSec(ts), } From cd348ce8c27e91d3a6f07e20cf083d0e2a34974e Mon Sep 17 00:00:00 2001 From: Tural Devrishev Date: Tue, 11 Aug 2026 13:57:08 +0300 Subject: [PATCH 2/4] *: attach full transactions to PrepareRequest Signed-off-by: Tural Devrishev --- CHANGELOG.md | 3 + config.go | 39 +++++++++-- context.go | 66 ++++++++++++------ dbft.go | 18 ++++- dbft_test.go | 97 ++++++++++++++++++++++++-- internal/consensus/consensus.go | 18 ++++- internal/consensus/constructors.go | 13 +++- internal/consensus/message_test.go | 10 +++ internal/consensus/prepare_request.go | 9 +++ internal/consensus/recovery_message.go | 3 +- internal/consensus/transaction.go | 5 ++ prepare_request.go | 4 ++ send.go | 7 +- 13 files changed, 252 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b26e7da4..208e5ba25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ This document outlines major changes between releases. ## [Unreleased] New features: + * `PrepareRequestExtensionEnablingHeight` configuration parameter and + `NewPrepareRequestExtended` callback to attach full transaction list to + `PrepareRequest` instead of hashes starting from the given height (#160) Behaviour changes: diff --git a/config.go b/config.go index d12644157..0493d25a7 100644 --- a/config.go +++ b/config.go @@ -32,6 +32,9 @@ type Config[H Hash] struct { // AntiMEVExtensionEnablingHeight denotes the height starting from which dBFT // Anti-MEV extensions should be enabled. -1 means no extension is enabled. AntiMEVExtensionEnablingHeight int64 + // PrepareRequestExtensionEnablingHeight denotes the height starting from which + // an extended PrepareRequest format should be enabled. -1 means no extension is enabled. + PrepareRequestExtensionEnablingHeight int64 // GetKeyPair returns an index of the node in the list of validators // together with it's key pair. GetKeyPair func([]PublicKey) (int, PrivateKey, PublicKey) @@ -81,8 +84,12 @@ type Config[H Hash] struct { GetValidators func(...Transaction[H]) []PublicKey // NewConsensusPayload is a constructor for payload.ConsensusPayload. NewConsensusPayload func(*Context[H], MessageType, any) ConsensusPayload[H] - // NewPrepareRequest is a constructor for payload.PrepareRequest. + // NewPrepareRequest is a constructor for payload.PrepareRequest that + // builds a request carrying transaction hashes only. NewPrepareRequest func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H] + // NewPrepareRequestExtended is a constructor for payload.PrepareRequest that + // builds a request carrying full transaction list. + NewPrepareRequestExtended func(ts uint64, nonce uint64, transactions []Transaction[H]) PrepareRequest[H] // NewPrepareResponse is a constructor for payload.PrepareResponse. NewPrepareResponse func(preparationHash H) PrepareResponse[H] // NewChangeView is a constructor for payload.ChangeView. @@ -137,9 +144,10 @@ func defaultConfig[H Hash]() *Config[H] { VerifyPrepareResponse: func(ConsensusPayload[H]) error { return nil }, VerifyCommit: func(ConsensusPayload[H]) error { return nil }, - AntiMEVExtensionEnablingHeight: -1, - VerifyPreBlock: func(PreBlock[H]) bool { return true }, - VerifyPreCommit: func(ConsensusPayload[H]) error { return nil }, + AntiMEVExtensionEnablingHeight: -1, + PrepareRequestExtensionEnablingHeight: -1, + VerifyPreBlock: func(PreBlock[H]) bool { return true }, + VerifyPreCommit: func(ConsensusPayload[H]) error { return nil }, } } @@ -204,6 +212,15 @@ func checkConfig[H Hash](cfg *Config[H]) error { return errors.New("NewPreCommit is set, but AntiMEVExtensionEnablingHeight is not specified") } } + if cfg.PrepareRequestExtensionEnablingHeight >= 0 { + if cfg.NewPrepareRequestExtended == nil { + return errors.New("NewPrepareRequestExtended is nil") + } + } else { + if cfg.NewPrepareRequestExtended != nil { + return errors.New("NewPrepareRequestExtended is set, but PrepareRequestExtensionEnablingHeight is not specified") + } + } if (cfg.MaxTimePerBlock == nil) != (cfg.SubscribeForTxs == nil) { return errors.New("MaxTimePerBlock and SubscribeForTxs should be specified/not specified at the same time") } @@ -253,6 +270,13 @@ func WithAntiMEVExtensionEnablingHeight[H Hash](h int64) func(config *Config[H]) } } +// WithPrepareRequestExtensionEnablingHeight sets PrepareRequestExtensionEnablingHeight. +func WithPrepareRequestExtensionEnablingHeight[H Hash](h int64) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.PrepareRequestExtensionEnablingHeight = h + } +} + // WithTimestampIncrement sets TimestampIncrement. func WithTimestampIncrement[H Hash](u uint64) func(config *Config[H]) { return func(cfg *Config[H]) { @@ -394,6 +418,13 @@ func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionHa } } +// WithNewPrepareRequestExtended sets NewPrepareRequestExtended. +func WithNewPrepareRequestExtended[H Hash](f func(ts uint64, nonce uint64, transactions []Transaction[H]) PrepareRequest[H]) func(config *Config[H]) { + return func(cfg *Config[H]) { + cfg.NewPrepareRequestExtended = f + } +} + // WithNewPrepareResponse sets NewPrepareResponse. func WithNewPrepareResponse[H Hash](f func(preparationHash H) PrepareResponse[H]) func(config *Config[H]) { return func(cfg *Config[H]) { diff --git a/context.go b/context.go index 28918446f..4a2f5b40f 100644 --- a/context.go +++ b/context.go @@ -56,12 +56,22 @@ type Context[H Hash] struct { Timestamp uint64 Nonce uint64 // TransactionHashes is a slice of hashes of proposed transactions in the current block. + // It's used when PrepareRequestExtensionEnabled is false. TransactionHashes []H // MissingTransactions is a slice of hashes containing missing transactions for the current block. + // It's used when PrepareRequestExtensionEnabled is false. MissingTransactions []H // Transactions is a map containing actual transactions for the current block. + // It's used when PrepareRequestExtensionEnabled is false. Transactions map[H]Transaction[H] + // TransactionList is a full ordered list of transactions proposed in the current block. + // It's used when PrepareRequestExtensionEnabled is true. + TransactionList []Transaction[H] + // PrepareRequestExtensionEnabled tells whether full transaction list is used for the + // PrepareRequest construction. Refreshed once per dBFT reset. + PrepareRequestExtensionEnabled bool + // PreparationPayloads stores consensus Prepare* payloads for the current epoch. PreparationPayloads []ConsensusPayload[H] // PreCommitPayloads stores consensus PreCommit payloads sent through all epochs @@ -287,6 +297,7 @@ func (c *Context[H]) reset(view byte, ts uint64) { } c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n) + c.TransactionList = nil if c.Transactions == nil { // Init. c.Transactions = make(map[H]Transaction[H]) } else { // Regular use. @@ -302,6 +313,7 @@ func (c *Context[H]) reset(view byte, ts uint64) { if c.MyIndex >= 0 { c.LastSeenMessage[c.MyIndex] = &HeightView{c.BlockIndex, c.ViewNumber} } + c.PrepareRequestExtensionEnabled = c.isPrepareRequestExtensionEnabled() } func emptyReusableSlice[E any](s []E, n int) []E { @@ -325,12 +337,15 @@ func (c *Context[H]) Fill(force bool) bool { _, _ = rand.Read(b) c.Nonce = binary.LittleEndian.Uint64(b) - c.TransactionHashes = make([]H, len(txx)) - - for i := range txx { - h := txx[i].Hash() - c.TransactionHashes[i] = h - c.Transactions[h] = txx[i] + if c.PrepareRequestExtensionEnabled { + c.TransactionList = txx + } else { + c.TransactionHashes = make([]H, len(txx)) + for i := range txx { + h := txx[i].Hash() + c.TransactionHashes[i] = h + c.Transactions[h] = txx[i] + } } c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement @@ -353,18 +368,12 @@ func (c *Context[H]) CreateBlock() Block[H] { return nil } - txx := make([]Transaction[H], len(c.TransactionHashes)) - - for i, h := range c.TransactionHashes { - txx[i] = c.Transactions[h] - } - // Anti-MEV extension properly sets PreBlock transactions once during PreBlock // construction and then never updates these transactions in the dBFT context. // Thus, user must not reuse txx if anti-MEV extension is enabled. However, // we don't skip a call to Block.SetTransactions since it may be used as a // signal to the user's code to finalize the block. - c.block.SetTransactions(txx) + c.block.SetTransactions(c.collectTransactions()) } return c.block @@ -377,24 +386,39 @@ func (c *Context[H]) CreatePreBlock() PreBlock[H] { return nil } - txx := make([]Transaction[H], len(c.TransactionHashes)) - - for i, h := range c.TransactionHashes { - txx[i] = c.Transactions[h] - } - - c.preBlock.SetTransactions(txx) + c.preBlock.SetTransactions(c.collectTransactions()) } return c.preBlock } +// collectTransactions returns the ordered list of transactions to be proposed in +// the resulting block. If PrepareRequestExtension is enabled, they are taken +// directly from TransactionList; otherwise they are reconstructed, preserving +// TransactionHashes order, from the already-collected Transactions map. +func (c *Context[H]) collectTransactions() []Transaction[H] { + if c.PrepareRequestExtensionEnabled { + return c.TransactionList + } + txx := make([]Transaction[H], len(c.TransactionHashes)) + for i, h := range c.TransactionHashes { + txx[i] = c.Transactions[h] + } + return txx +} + // isAntiMEVExtensionEnabled returns whether Anti-MEV dBFT extension is enabled // at the currently processing block height. func (c *Context[H]) isAntiMEVExtensionEnabled() bool { return c.Config.AntiMEVExtensionEnablingHeight >= 0 && uint32(c.Config.AntiMEVExtensionEnablingHeight) <= c.BlockIndex } +// isPrepareRequestExtensionEnabled returns whether PrepareRequest dBFT extension is enabled +// at the currently processing block height. +func (c *Context[H]) isPrepareRequestExtensionEnabled() bool { + return c.Config.PrepareRequestExtensionEnablingHeight >= 0 && uint32(c.Config.PrepareRequestExtensionEnablingHeight) <= c.BlockIndex +} + // MakeHeader returns half-filled block for the current epoch. // All hashable fields will be filled. func (c *Context[H]) MakeHeader() Block[H] { @@ -432,7 +456,7 @@ func (c *Context[H]) MakePreHeader() PreBlock[H] { // hasAllTransactions returns true iff all transactions were received // for the proposed block. func (c *Context[H]) hasAllTransactions() bool { - return len(c.TransactionHashes) == len(c.Transactions) + return c.PrepareRequestExtensionEnabled || len(c.TransactionHashes) == len(c.Transactions) } func (c *Context[H]) subscribeForTransactions() { diff --git a/dbft.go b/dbft.go index c97c0166f..23c34df32 100644 --- a/dbft.go +++ b/dbft.go @@ -51,6 +51,9 @@ func New[H Hash](options ...func(config *Config[H])) (*DBFT[H], error) { } func (d *DBFT[H]) addTransaction(tx Transaction[H]) { + if d.PrepareRequestExtensionEnabled { + panic("bug: addTransaction must not be called with PrepareRequestExtension active") + } d.Transactions[tx.Hash()] = tx if d.hasAllTransactions() { if d.IsPrimary() || d.Context.WatchOnly() { @@ -349,10 +352,19 @@ func (d *DBFT[H]) onPrepareRequest(msg ConsensusPayload[H]) { d.Timestamp = p.Timestamp() d.Nonce = p.Nonce() - d.TransactionHashes = p.TransactionHashes() + var txCount int + if d.PrepareRequestExtensionEnabled { + d.TransactionList = p.Transactions() + txCount = len(d.TransactionList) + } else { + d.TransactionHashes = p.TransactionHashes() + txCount = len(d.TransactionHashes) + } - d.Logger.Info("received PrepareRequest", zap.Uint16("validator", msg.ValidatorIndex()), zap.Int("tx", len(d.TransactionHashes))) - d.processMissingTx() + d.Logger.Info("received PrepareRequest", zap.Uint16("validator", msg.ValidatorIndex()), zap.Int("tx", txCount)) + if !d.PrepareRequestExtensionEnabled { + d.processMissingTx() + } d.updateExistingPayloads(msg) d.PreparationPayloads[msg.ValidatorIndex()] = msg diff --git a/dbft_test.go b/dbft_test.go index a151a48ef..14cf0dcbb 100644 --- a/dbft_test.go +++ b/dbft_test.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "encoding/binary" "fmt" + "slices" "testing" "time" @@ -577,7 +578,22 @@ func TestDBFT_Invalid(t *testing.T) { opts = append(opts, dbft.WithNewRecoveryMessage[crypto.Uint256](func() dbft.RecoveryMessage[crypto.Uint256] { return nil - }), dbft.WithMaxTimePerBlock[crypto.Uint256](func() time.Duration { + })) + t.Run("NewPrepareRequestExtended without PrepareRequestExtensionEnablingHeight", func(t *testing.T) { + invalidOpts := append(slices.Clone(opts), dbft.WithNewPrepareRequestExtended[crypto.Uint256](func(uint64, uint64, []dbft.Transaction[crypto.Uint256]) dbft.PrepareRequest[crypto.Uint256] { + return nil + })) + _, err := dbft.New(invalidOpts...) + require.ErrorContains(t, err, "NewPrepareRequestExtended is set, but PrepareRequestExtensionEnablingHeight is not specified") + }) + + t.Run("PrepareRequestExtensionEnablingHeight without NewPrepareRequestExtended", func(t *testing.T) { + invalidOpts := append(slices.Clone(opts), dbft.WithPrepareRequestExtensionEnablingHeight[crypto.Uint256](0)) + _, err := dbft.New(invalidOpts...) + require.ErrorContains(t, err, "NewPrepareRequestExtended is nil") + }) + + opts = append(opts, dbft.WithMaxTimePerBlock[crypto.Uint256](func() time.Duration { return 0 })) t.Run("MaxTimePerBlock without SubscribeForTxs", func(t *testing.T) { @@ -1023,6 +1039,52 @@ func TestDBFT_CachedMessages(t *testing.T) { } } +func TestDBFT_PrepareRequestExtensionEnabledSwitch(t *testing.T) { + s := newTestState(0, 1) + + opts := s.getOptions() + opts = append(opts, + dbft.WithPrepareRequestExtensionEnablingHeight[crypto.Uint256](2), + dbft.WithNewPrepareRequestExtended[crypto.Uint256](consensus.NewPrepareRequestExtended)) + + service, err := dbft.New[crypto.Uint256](opts...) + require.NoError(t, err) + + service.Start(0) + require.False(t, service.PrepareRequestExtensionEnabled) + + p := s.tryRecv() + require.NotNil(t, p) + require.Equal(t, dbft.PrepareRequestType, p.Type()) + require.EqualValues(t, s.currHeight+1, p.Height()) + + cm := s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.CommitType, cm.Type()) + + b := s.nextBlock() + require.NotNil(t, b) + require.Equal(t, s.currHeight+1, b.Index()) + + s.currHeight++ + service.Reset(0) + require.True(t, service.PrepareRequestExtensionEnabled) + service.OnTimeout(s.currHeight+1, 0) + + p = s.tryRecv() + require.NotNil(t, p) + require.Equal(t, dbft.PrepareRequestType, p.Type()) + require.EqualValues(t, s.currHeight+1, p.Height()) + + cm = s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.CommitType, cm.Type()) + + b = s.nextBlock() + require.NotNil(t, b) + require.Equal(t, s.currHeight+1, b.Index()) +} + func (s testState) getChangeView(from uint16, view byte) Payload { cv := consensus.NewChangeView(view, 0, 0) @@ -1197,24 +1259,47 @@ func (s *testState) getAMEVOptions() []func(*dbft.Config[crypto.Uint256]) { return opts } -func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { +func txHashesFromList(ctx *dbft.Context[crypto.Uint256]) []crypto.Uint256 { + txHashes := make([]crypto.Uint256, len(ctx.TransactionList)) + for i, tx := range ctx.TransactionList { + txHashes[i] = tx.Hash() + } + return txHashes +} + +func blockTxHashes(ctx *dbft.Context[crypto.Uint256]) ([]crypto.Uint256, bool) { + if ctx.PrepareRequestExtensionEnabled { + if ctx.TransactionList == nil { + return nil, false + } + return txHashesFromList(ctx), true + } if ctx.TransactionHashes == nil { + return nil, false + } + return ctx.TransactionHashes, true +} + +func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { + txHashes, ok := blockTxHashes(ctx) + if !ok { return nil } - block := consensus.NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + block := consensus.NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, txHashes) return block } func newPreBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.PreBlock[crypto.Uint256] { - if ctx.TransactionHashes == nil { + txHashes, ok := blockTxHashes(ctx) + if !ok { return nil } - pre := consensus.NewPreBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + pre := consensus.NewPreBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, txHashes) return pre } func newAMEVBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { - if ctx.TransactionHashes == nil { + if _, ok := blockTxHashes(ctx); !ok { return nil } var data [][]byte diff --git a/internal/consensus/consensus.go b/internal/consensus/consensus.go index 459f6c92f..a4a2ae408 100644 --- a/internal/consensus/consensus.go +++ b/internal/consensus/consensus.go @@ -58,10 +58,22 @@ func New(logger *zap.Logger, key dbft.PrivateKey, pub dbft.PublicKey, } func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { - if ctx.TransactionHashes == nil { - return nil + var txHashes []crypto.Uint256 + if ctx.PrepareRequestExtensionEnabled { + if ctx.TransactionList == nil { + return nil + } + txHashes = make([]crypto.Uint256, len(ctx.TransactionList)) + for i, tx := range ctx.TransactionList { + txHashes[i] = tx.Hash() + } + } else { + if ctx.TransactionHashes == nil { + return nil + } + txHashes = ctx.TransactionHashes } - block := NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + block := NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, txHashes) return block } diff --git a/internal/consensus/constructors.go b/internal/consensus/constructors.go index 9a0636b96..2d7544dad 100644 --- a/internal/consensus/constructors.go +++ b/internal/consensus/constructors.go @@ -20,7 +20,8 @@ func NewConsensusPayload(t dbft.MessageType, height uint32, validatorIndex uint1 } } -// NewPrepareRequest returns minimal prepareRequest implementation. +// NewPrepareRequest returns minimal prepareRequest implementation carrying +// transaction hashes only. func NewPrepareRequest(ts uint64, nonce uint64, transactionHashes []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { return &prepareRequest{ transactionHashes: transactionHashes, @@ -29,6 +30,16 @@ func NewPrepareRequest(ts uint64, nonce uint64, transactionHashes []crypto.Uint2 } } +// NewPrepareRequestExtended returns minimal prepareRequest implementation +// carrying full transaction list. +func NewPrepareRequestExtended(ts uint64, nonce uint64, transactions []dbft.Transaction[crypto.Uint256]) dbft.PrepareRequest[crypto.Uint256] { + return &prepareRequest{ + transactions: transactions, + nonce: nonce, + timestamp: nanoSecToSec(ts), + } +} + // NewPrepareResponse returns minimal PrepareResponse implementation. func NewPrepareResponse(preparationHash crypto.Uint256) dbft.PrepareResponse[crypto.Uint256] { return &prepareResponse{ diff --git a/internal/consensus/message_test.go b/internal/consensus/message_test.go index ff149cd28..e4bacf897 100644 --- a/internal/consensus/message_test.go +++ b/internal/consensus/message_test.go @@ -18,6 +18,7 @@ func TestPayload_EncodeDecode(t *testing.T) { } t.Run("PrepareRequest", func(t *testing.T) { + tx1, tx2 := Tx64(1), Tx64(2) m := generateMessage(dbft.PrepareRequestType, &prepareRequest{ nonce: 123, timestamp: 345, @@ -25,6 +26,10 @@ func TestPayload_EncodeDecode(t *testing.T) { {1, 2, 3}, {5, 6, 7}, }, + transactions: []dbft.Transaction[crypto.Uint256]{ + &tx1, + &tx2, + }, }) testEncodeDecode(t, m, new(Payload)) @@ -60,6 +65,7 @@ func TestPayload_EncodeDecode(t *testing.T) { }) t.Run("RecoveryMessage", func(t *testing.T) { + tx1, tx2 := Tx64(1), Tx64(2) m := generateMessage(dbft.RecoveryMessageType, &recoveryMessage{ changeViewPayloads: []changeViewCompact{ { @@ -81,6 +87,10 @@ func TestPayload_EncodeDecode(t *testing.T) { {1, 2, 3}, {5, 6, 7}, }, + transactions: []dbft.Transaction[crypto.Uint256]{ + &tx1, + &tx2, + }, }, }) diff --git a/internal/consensus/prepare_request.go b/internal/consensus/prepare_request.go index 4955cb3e3..3acd56ad8 100644 --- a/internal/consensus/prepare_request.go +++ b/internal/consensus/prepare_request.go @@ -10,12 +10,14 @@ import ( type ( prepareRequest struct { transactionHashes []crypto.Uint256 + transactions []dbft.Transaction[crypto.Uint256] nonce uint64 timestamp uint32 } // prepareRequestAux is an auxiliary structure for prepareRequest encoding. prepareRequestAux struct { TransactionHashes []crypto.Uint256 + Transactions []dbft.Transaction[crypto.Uint256] Nonce uint64 Timestamp uint32 } @@ -27,6 +29,7 @@ var _ dbft.PrepareRequest[crypto.Uint256] = (*prepareRequest)(nil) func (p prepareRequest) EncodeBinary(w *gob.Encoder) error { return w.Encode(&prepareRequestAux{ TransactionHashes: p.transactionHashes, + Transactions: p.transactions, Nonce: p.nonce, Timestamp: p.timestamp, }) @@ -42,6 +45,7 @@ func (p *prepareRequest) DecodeBinary(r *gob.Decoder) error { p.timestamp = aux.Timestamp p.nonce = aux.Nonce p.transactionHashes = aux.TransactionHashes + p.transactions = aux.Transactions return nil } @@ -59,3 +63,8 @@ func (p prepareRequest) Nonce() uint64 { func (p prepareRequest) TransactionHashes() []crypto.Uint256 { return p.transactionHashes } + +// Transactions implements PrepareRequest interface. +func (p prepareRequest) Transactions() []dbft.Transaction[crypto.Uint256] { + return p.transactions +} diff --git a/internal/consensus/recovery_message.go b/internal/consensus/recovery_message.go index d6b43b184..5b90bea47 100644 --- a/internal/consensus/recovery_message.go +++ b/internal/consensus/recovery_message.go @@ -91,7 +91,8 @@ func (m *recoveryMessage) GetPrepareRequest(p dbft.ConsensusPayload[crypto.Uint2 // prepareRequest.Timestamp() here returns nanoseconds-precision value, so convert it to seconds again timestamp: nanoSecToSec(m.prepareRequest.Timestamp()), nonce: m.prepareRequest.Nonce(), - transactionHashes: m.prepareRequest.TransactionHashes(), + transactionHashes: m.prepareRequest.(*prepareRequest).transactionHashes, + transactions: m.prepareRequest.(*prepareRequest).transactions, }) req.SetValidatorIndex(ind) diff --git a/internal/consensus/transaction.go b/internal/consensus/transaction.go index cc4f3b90d..eeb2a331e 100644 --- a/internal/consensus/transaction.go +++ b/internal/consensus/transaction.go @@ -2,6 +2,7 @@ package consensus import ( "encoding/binary" + "encoding/gob" "errors" "github.com/nspcc-dev/dbft" @@ -16,6 +17,10 @@ type Tx64 uint64 var _ dbft.Transaction[crypto.Uint256] = (*Tx64)(nil) +func init() { + gob.Register(new(Tx64)) +} + func (t *Tx64) Hash() (h crypto.Uint256) { binary.LittleEndian.PutUint64(h[:], uint64(*t)) return diff --git a/prepare_request.go b/prepare_request.go index 3ba594c9c..34e40a74e 100644 --- a/prepare_request.go +++ b/prepare_request.go @@ -7,5 +7,9 @@ type PrepareRequest[H Hash] interface { // Nonce is a random nonce. Nonce() uint64 // TransactionHashes returns hashes of all transaction in a proposed block. + // It's used when PrepareRequestExtensionEnabled is false. TransactionHashes() []H + // Transactions returns full transaction list attached to this PrepareRequest. + // It's used when PrepareRequestExtensionEnabled is true. + Transactions() []Transaction[H] } diff --git a/send.go b/send.go index 6c40d952e..a6c0a2c2c 100644 --- a/send.go +++ b/send.go @@ -21,7 +21,12 @@ func (c *Context[H]) makePrepareRequest(force bool) ConsensusPayload[H] { return nil } - req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes) + var req PrepareRequest[H] + if c.PrepareRequestExtensionEnabled { + req = c.Config.NewPrepareRequestExtended(c.Timestamp, c.Nonce, c.TransactionList) + } else { + req = c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes) + } return c.Config.NewConsensusPayload(c, PrepareRequestType, req) } From e0f831235ad65b389a6f1485576bd25d5e89f99a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 23 Sep 2026 16:58:01 +0300 Subject: [PATCH 3/4] context: change the definition of (*Context).hasAllTransactions 1. Do not rely on PrepareRequestExtension-disabled-specific Transactions/TransactionHashes fields to detect missing transactions. Instead, use the MissingTransactions map to detect if all transactions are fetched. 2. Change the order of MissingTransactions update and addTransaction call to ensure addTransaction relies on up-to-date MissingTransactions state. Verify other users of MissingTransactions against the similar problem. Signed-off-by: Anna Shaleva --- context.go | 2 +- dbft.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/context.go b/context.go index 4a2f5b40f..386627522 100644 --- a/context.go +++ b/context.go @@ -456,7 +456,7 @@ func (c *Context[H]) MakePreHeader() PreBlock[H] { // hasAllTransactions returns true iff all transactions were received // for the proposed block. func (c *Context[H]) hasAllTransactions() bool { - return c.PrepareRequestExtensionEnabled || len(c.TransactionHashes) == len(c.Transactions) + return len(c.MissingTransactions) == 0 } func (c *Context[H]) subscribeForTransactions() { diff --git a/dbft.go b/dbft.go index 23c34df32..c477bce4f 100644 --- a/dbft.go +++ b/dbft.go @@ -181,13 +181,8 @@ func (d *DBFT[H]) OnTransaction(tx Transaction[H]) { if i < 0 { return } - d.addTransaction(tx) - // `addTransaction` checks for responses and commits. If this was the last transaction - // Context could be initialized on a new height, clearing this field. - if len(d.MissingTransactions) == 0 { - return - } d.MissingTransactions = slices.Delete(d.MissingTransactions, i, i+1) + d.addTransaction(tx) } // OnTimeout advances state machine as if timeout was fired. From 339f9cd0d5893cb09fd1f09ec668f5cac9f07c85 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 23 Sep 2026 17:19:14 +0300 Subject: [PATCH 4/4] WIP dbft: request missing blobs Signed-off-by: Anna Shaleva --- config.go | 2 ++ context.go | 6 ++++-- dbft.go | 31 +++++++++++++++++++++++-------- transaction.go | 3 +++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/config.go b/config.go index 0493d25a7..849c61f1f 100644 --- a/config.go +++ b/config.go @@ -56,6 +56,8 @@ type Config[H Hash] struct { StopTxFlow func() // GetTx returns a transaction from memory pool. GetTx func(h H) Transaction[H] + // GetTxData returns arbitrary verified data associated with the transaction. + GetTxData func(h H) any // GetVerified returns a slice of verified transactions // to be proposed in a new block. GetVerified func() []Transaction[H] diff --git a/context.go b/context.go index 386627522..6e80a2899 100644 --- a/context.go +++ b/context.go @@ -58,8 +58,10 @@ type Context[H Hash] struct { // TransactionHashes is a slice of hashes of proposed transactions in the current block. // It's used when PrepareRequestExtensionEnabled is false. TransactionHashes []H - // MissingTransactions is a slice of hashes containing missing transactions for the current block. - // It's used when PrepareRequestExtensionEnabled is false. + // MissingTransactions is a slice of hashes containing missing transactions (in case of + // disabled PrepareRequestExtension) or hashes of those transactions that require + // additional data to be fetched prior to the proposal verification (in case of enabled + // PrepareRequestExtension). MissingTransactions []H // Transactions is a map containing actual transactions for the current block. // It's used when PrepareRequestExtensionEnabled is false. diff --git a/dbft.go b/dbft.go index c477bce4f..991eed338 100644 --- a/dbft.go +++ b/dbft.go @@ -50,11 +50,14 @@ func New[H Hash](options ...func(config *Config[H])) (*DBFT[H], error) { return d, nil } +// addTransaction adds a missing transaction to the context (in case of disabled +// PrepareRequestExtension) and advances the state machine if all transactions +// (or dependent data in case of enabled PrepareRequestExtension) are collected +// and it's possible to build a valid block. func (d *DBFT[H]) addTransaction(tx Transaction[H]) { - if d.PrepareRequestExtensionEnabled { - panic("bug: addTransaction must not be called with PrepareRequestExtension active") + if !d.PrepareRequestExtensionEnabled { + d.Transactions[tx.Hash()] = tx } - d.Transactions[tx.Hash()] = tx if d.hasAllTransactions() { if d.IsPrimary() || d.Context.WatchOnly() { return @@ -162,8 +165,9 @@ func (d *DBFT[H]) initializeConsensus(view byte, ts uint64) { d.changeTimer(timeout) } -// OnTransaction notifies service about receiving new transaction from the -// proposed list of transactions. +// OnTransaction notifies service about receiving new transaction (in case of +// disabled PrepareRequestExtension) or transaction data (in case of enabled +// PrepareRequestExtension) from the proposed list of transactions. func (d *DBFT[H]) OnTransaction(tx Transaction[H]) { // d.Logger.Debug("OnTransaction", // zap.Bool("backup", d.IsBackup()), @@ -357,9 +361,7 @@ func (d *DBFT[H]) onPrepareRequest(msg ConsensusPayload[H]) { } d.Logger.Info("received PrepareRequest", zap.Uint16("validator", msg.ValidatorIndex()), zap.Int("tx", txCount)) - if !d.PrepareRequestExtensionEnabled { - d.processMissingTx() - } + d.processMissingTx() d.updateExistingPayloads(msg) d.PreparationPayloads[msg.ValidatorIndex()] = msg @@ -372,6 +374,19 @@ func (d *DBFT[H]) onPrepareRequest(msg ConsensusPayload[H]) { } func (d *DBFT[H]) processMissingTx() { + if d.PrepareRequestExtensionEnabled { + for _, tx := range d.TransactionList { + if tx.HasData() { + continue + } + h := tx.Hash() + if data := d.GetTxData(tx.Hash()); data == nil { + d.MissingTransactions = append(d.MissingTransactions, h) + } + } + + return + } for _, h := range d.TransactionHashes { if _, ok := d.Transactions[h]; ok { continue diff --git a/transaction.go b/transaction.go index ae3277c71..b434f8c42 100644 --- a/transaction.go +++ b/transaction.go @@ -5,4 +5,7 @@ type Transaction[H Hash] interface { // Hash must return cryptographic hash of the transaction. // Transactions which have equal hashes are considered equal. Hash() H + // HasData returns true if the transaction has the associated and valid data + // fetched. + HasData() bool }