diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b2daa03..536bf4247 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,11 @@ This document outlines major changes between releases. New features: Behaviour changes: + * transactions management refactoring implied library API changes (#164) Improvements: * minimum required Go version is 1.26 (#144, #156, #162) - * `go.uber.org/zap` dependency upgrade from v1.27.0 to v1.27.1 (#156) + * `go.uber.org/zap` dependency upgrade from v1.27.0 to v1.27.1 (#156) Bugs fixed: diff --git a/config.go b/config.go index d12644157..43fcfd216 100644 --- a/config.go +++ b/config.go @@ -2,6 +2,7 @@ package dbft import ( "errors" + "iter" "time" "go.uber.org/zap" @@ -42,7 +43,7 @@ type Config[H Hash] struct { // RequestTx is a callback which is called when transaction contained // in current block can't be found in memory pool. The slice received by // this callback MUST NOT be changed. - RequestTx func(h ...H) + RequestTx func(h iter.Seq[H]) // SubscribeForTxs is a callback which is called when dBFT needs to track incoming // mempool transactions. Subscription is supposed to be single-use, no unsubscription // is initiated by dBFT, hence it's the user's duty to manage and release resources. @@ -51,8 +52,6 @@ type Config[H Hash] struct { // StopTxFlow is a callback which is called when the process no longer needs // any transactions. StopTxFlow func() - // GetTx returns a transaction from memory pool. - GetTx func(h H) Transaction[H] // GetVerified returns a slice of verified transactions // to be proposed in a new block. GetVerified func() []Transaction[H] @@ -82,7 +81,7 @@ type Config[H Hash] struct { // NewConsensusPayload is a constructor for payload.ConsensusPayload. NewConsensusPayload func(*Context[H], MessageType, any) ConsensusPayload[H] // NewPrepareRequest is a constructor for payload.PrepareRequest. - NewPrepareRequest func(ts uint64, nonce uint64, transactionHashes []H) PrepareRequest[H] + NewPrepareRequest func(ts uint64, nonce uint64, txes []Transaction[H]) PrepareRequest[H] // NewPrepareResponse is a constructor for payload.PrepareResponse. NewPrepareResponse func(preparationHash H) PrepareResponse[H] // NewChangeView is a constructor for payload.ChangeView. @@ -120,9 +119,8 @@ func defaultConfig[H Hash]() *Config[H] { TimePerBlock: func() time.Duration { return defaultSecondsPerBlock }, TimestampIncrement: defaultTimestampIncrement, GetKeyPair: nil, - RequestTx: func(...H) {}, + RequestTx: func(iter.Seq[H]) {}, StopTxFlow: func() {}, - GetTx: func(H) Transaction[H] { return nil }, GetVerified: func() []Transaction[H] { return make([]Transaction[H], 0) }, VerifyBlock: func(Block[H]) bool { return true }, Broadcast: func(ConsensusPayload[H]) {}, @@ -275,7 +273,7 @@ func WithNewBlockFromContext[H Hash](f func(ctx *Context[H]) Block[H]) func(conf } // WithRequestTx sets RequestTx. -func WithRequestTx[H Hash](f func(h ...H)) func(config *Config[H]) { +func WithRequestTx[H Hash](f func(hs iter.Seq[H])) func(config *Config[H]) { return func(cfg *Config[H]) { cfg.RequestTx = f } @@ -295,13 +293,6 @@ func WithStopTxFlow[H Hash](f func()) func(config *Config[H]) { } } -// WithGetTx sets GetTx. -func WithGetTx[H Hash](f func(h H) Transaction[H]) func(config *Config[H]) { - return func(cfg *Config[H]) { - cfg.GetTx = f - } -} - // WithGetVerified sets GetVerified. func WithGetVerified[H Hash](f func() []Transaction[H]) func(config *Config[H]) { return func(cfg *Config[H]) { @@ -388,7 +379,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, transactionHashes []H) PrepareRequest[H]) func(config *Config[H]) { +func WithNewPrepareRequest[H Hash](f func(ts uint64, nonce uint64, transactionHashes []Transaction[H]) PrepareRequest[H]) func(config *Config[H]) { return func(cfg *Config[H]) { cfg.NewPrepareRequest = f } diff --git a/context.go b/context.go index 28918446f..b0e609106 100644 --- a/context.go +++ b/context.go @@ -55,12 +55,12 @@ type Context[H Hash] struct { // Timestamp is a nanosecond-precision timestamp Timestamp uint64 Nonce uint64 - // TransactionHashes is a slice of hashes of proposed transactions in the current block. - TransactionHashes []H - // MissingTransactions is a slice of hashes containing missing transactions for the current block. - MissingTransactions []H - // Transactions is a map containing actual transactions for the current block. - Transactions map[H]Transaction[H] + // Transactions is a slice containing actual transactions for the current block. + Transactions []Transaction[H] + // MissingTransactions is a map of a missing transaction hash to its index in + // the slice of proposed transactions for the current block. This map is + // managed by dBFT and should not be changed by the user. + MissingTransactions map[H]int // PreparationPayloads stores consensus Prepare* payloads for the current epoch. PreparationPayloads []ConsensusPayload[H] @@ -287,15 +287,8 @@ func (c *Context[H]) reset(view byte, ts uint64) { } c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n) - if c.Transactions == nil { // Init. - c.Transactions = make(map[H]Transaction[H]) - } else { // Regular use. - clear(c.Transactions) - } - c.TransactionHashes = nil - if c.MissingTransactions != nil { - c.MissingTransactions = c.MissingTransactions[:0] - } + c.Transactions = nil // overwritten by the user on every proposal, no need for manual clear management. + c.MissingTransactions = nil // overwritten by the user on every proposal, no need for manual clear management. c.PrimaryIndex = c.GetPrimaryIndex(view) c.ViewNumber = view @@ -325,13 +318,7 @@ 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] - } + c.Transactions = txx c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement if now := c.getTimestamp(); now > c.Timestamp { @@ -353,18 +340,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, + // Thus, user must not reuse c.Transactions 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.Transactions) } return c.block @@ -377,13 +358,7 @@ 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.Transactions) } return c.preBlock @@ -432,7 +407,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 len(c.MissingTransactions) == 0 } func (c *Context[H]) subscribeForTransactions() { diff --git a/dbft.go b/dbft.go index c97c0166f..632621d18 100644 --- a/dbft.go +++ b/dbft.go @@ -2,7 +2,7 @@ package dbft import ( "fmt" - "slices" + "maps" "sync" "time" @@ -51,7 +51,12 @@ func New[H Hash](options ...func(config *Config[H])) (*DBFT[H], error) { } func (d *DBFT[H]) addTransaction(tx Transaction[H]) { - d.Transactions[tx.Hash()] = tx + i, ok := d.MissingTransactions[tx.Hash()] + if !ok { + return + } + d.Transactions[i] = tx + delete(d.MissingTransactions, tx.Hash()) if d.hasAllTransactions() { if d.IsPrimary() || d.Context.WatchOnly() { return @@ -170,21 +175,11 @@ func (d *DBFT[H]) OnTransaction(tx Transaction[H]) { // zap.Bool("block_sent", d.BlockSent())) if !d.IsBackup() || d.NotAcceptingPayloadsDueToViewChanging() || !d.RequestSentOrReceived() || d.ResponseSent() || d.PreCommitSent() || - d.CommitSent() || d.BlockSent() || len(d.MissingTransactions) == 0 { + d.CommitSent() || d.BlockSent() || d.hasAllTransactions() { return } - i := slices.Index(d.MissingTransactions, tx.Hash()) - 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) } // OnTimeout advances state machine as if timeout was fired. @@ -349,12 +344,12 @@ func (d *DBFT[H]) onPrepareRequest(msg ConsensusPayload[H]) { d.Timestamp = p.Timestamp() d.Nonce = p.Nonce() - d.TransactionHashes = p.TransactionHashes() + d.Transactions, d.MissingTransactions = p.Transactions() - 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", len(d.Transactions))) d.updateExistingPayloads(msg) d.PreparationPayloads[msg.ValidatorIndex()] = msg + d.processMissingTx() if !d.hasAllTransactions() || !d.createAndCheckBlock() || d.Context.WatchOnly() { return @@ -364,22 +359,12 @@ func (d *DBFT[H]) onPrepareRequest(msg ConsensusPayload[H]) { d.checkPrepare() } +// processMissingTx fills in the map of missing transactions and requests them. func (d *DBFT[H]) processMissingTx() { - for _, h := range d.TransactionHashes { - if _, ok := d.Transactions[h]; ok { - continue - } - if tx := d.GetTx(h); tx == nil { - d.MissingTransactions = append(d.MissingTransactions, h) - } else { - d.Transactions[h] = tx - } - } - if len(d.MissingTransactions) != 0 { d.Logger.Info("missing tx", zap.Int("count", len(d.MissingTransactions))) - d.RequestTx(d.MissingTransactions...) + d.RequestTx(maps.Keys(d.MissingTransactions)) } } diff --git a/dbft_test.go b/dbft_test.go index 18834ad2f..983486ee5 100644 --- a/dbft_test.go +++ b/dbft_test.go @@ -1,8 +1,8 @@ package dbft_test import ( - "encoding/binary" "fmt" + "iter" "testing" "time" @@ -24,19 +24,11 @@ type testState struct { ch []Payload currHeight uint32 currHash crypto.Uint256 - pool *testPool preBlocks []dbft.PreBlock[crypto.Uint256] blocks []dbft.Block[crypto.Uint256] verify func(b dbft.Block[crypto.Uint256]) bool } -type ( - testTx uint64 - testPool struct { - storage map[crypto.Uint256]testTx - } -) - const debugTests = false func TestDBFT_OnStartPrimarySendPrepareRequest(t *testing.T) { @@ -137,7 +129,7 @@ func TestDBFT_OnReceiveRequestSendResponse(t *testing.T) { s := newTestState(2, 7) s.verify = func(b dbft.Block[crypto.Uint256]) bool { for _, tx := range b.Transactions() { - if tx.(testTx)%10 == 0 { + if *tx.(*consensus.Tx64)%10 == 0 { return false } } @@ -148,10 +140,9 @@ func TestDBFT_OnReceiveRequestSendResponse(t *testing.T) { t.Run("receive request from primary", func(t *testing.T) { s.currHeight = 4 service, _ := dbft.New[crypto.Uint256](s.getOptions()...) - txs := []testTx{1} - s.pool.Add(txs[0]) + txs := []*consensus.Tx64{new(consensus.Tx64(1))} - p := s.getPrepareRequest(5, txs[0].Hash()) + p := s.getPrepareRequest(5, txs[0]) service.Start(0) service.OnReceive(p) @@ -180,7 +171,7 @@ func TestDBFT_OnReceiveRequestSendResponse(t *testing.T) { t.Run("change view on invalid tx", func(t *testing.T) { s.currHeight = 4 service, _ := dbft.New[crypto.Uint256](s.getOptions()...) - txs := []testTx{10} + txs := []*consensus.Tx64{new(consensus.Tx64(10))} service.Start(0) @@ -188,12 +179,12 @@ func TestDBFT_OnReceiveRequestSendResponse(t *testing.T) { service.LastSeenMessage[i] = &dbft.HeightView{s.currHeight + 1, 0} } - p := s.getPrepareRequest(5, txs[0].Hash()) + p := s.getPrepareRequestWithMissing(5, []dbft.Transaction[crypto.Uint256]{txs[0]}, 0) service.OnReceive(p) require.Nil(t, s.tryRecv()) - service.OnTransaction(testTx(10)) + service.OnTransaction(new(consensus.Tx64(10))) cv := s.tryRecv() require.NotNil(t, cv) @@ -208,25 +199,24 @@ func TestDBFT_OnReceiveRequestSendResponse(t *testing.T) { t.Run("receive invalid prepare request", func(t *testing.T) { s.currHeight = 4 service, _ := dbft.New[crypto.Uint256](s.getOptions()...) - txs := []testTx{1, 2} - s.pool.Add(txs[0]) + txs := []*consensus.Tx64{new(consensus.Tx64(1)), new(consensus.Tx64(2))} service.Start(0) t.Run("wrong primary index", func(t *testing.T) { - p := s.getPrepareRequest(4, txs[0].Hash()) + p := s.getPrepareRequest(4, txs[0]) service.OnReceive(p) require.Nil(t, s.tryRecv()) }) t.Run("old height", func(t *testing.T) { - p := s.getPrepareRequestWithHeight(5, 3, txs[0].Hash()) + p := s.getPrepareRequestWithHeight(5, 3, txs[0]) service.OnReceive(p) require.Nil(t, s.tryRecv()) }) t.Run("does not have all transactions", func(t *testing.T) { - p := s.getPrepareRequest(5, txs[0].Hash(), txs[1].Hash()) + p := s.getPrepareRequestWithMissing(5, []dbft.Transaction[crypto.Uint256]{txs[0], txs[1]}, 1) service.OnReceive(p) require.Nil(t, s.tryRecv()) @@ -259,8 +249,8 @@ func TestDBFT_CommitOnTransaction(t *testing.T) { srv.Start(0) require.Nil(t, s.tryRecv()) - tx := testTx(42) - req := s.getPrepareRequest(2, tx.Hash()) + tx := new(consensus.Tx64(42)) + req := s.getPrepareRequestWithMissing(2, []dbft.Transaction[crypto.Uint256]{tx}, 0) srv.OnReceive(req) srv.OnReceive(s.getPrepareResponse(1, req.Hash(), 0)) srv.OnReceive(s.getPrepareResponse(3, req.Hash(), 0)) @@ -269,17 +259,16 @@ func TestDBFT_CommitOnTransaction(t *testing.T) { // Test state for forming header. s1 := &testState{ count: s.count, - pool: newTestPool(), currHeight: 1, pubs: s.pubs, privs: s.privs, } - s1.pool.Add(tx) + reqFull := s.getPrepareRequest(2, tx) srv1, _ := dbft.New[crypto.Uint256](s1.getOptions()...) srv1.Start(0) - srv1.OnReceive(req) - srv1.OnReceive(s1.getPrepareResponse(1, req.Hash(), 0)) - srv1.OnReceive(s1.getPrepareResponse(3, req.Hash(), 0)) + srv1.OnReceive(reqFull) + srv1.OnReceive(s1.getPrepareResponse(1, reqFull.Hash(), 0)) + srv1.OnReceive(s1.getPrepareResponse(3, reqFull.Hash(), 0)) require.NotNil(t, srv1.Header()) for _, i := range []uint16{1, 2, 3} { @@ -534,7 +523,7 @@ func TestDBFT_Invalid(t *testing.T) { require.Error(t, err) }) - opts = append(opts, dbft.WithNewPrepareRequest[crypto.Uint256](func(uint64, uint64, []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { + opts = append(opts, dbft.WithNewPrepareRequest[crypto.Uint256](func(uint64, uint64, []dbft.Transaction[crypto.Uint256]) dbft.PrepareRequest[crypto.Uint256] { return nil })) t.Run("without NewPrepareResponse", func(t *testing.T) { @@ -590,7 +579,6 @@ func TestDBFT_Invalid(t *testing.T) { require.NoError(t, err) require.NotNil(t, d) require.NotNil(t, d.RequestTx) - require.NotNil(t, d.GetTx) require.NotNil(t, d.GetVerified) require.NotNil(t, d.VerifyBlock) require.NotNil(t, d.Broadcast) @@ -1059,12 +1047,19 @@ func (s testState) getPrepareResponse(from uint16, phash crypto.Uint256, view by return p } -func (s testState) getPrepareRequest(from uint16, hashes ...crypto.Uint256) Payload { - return s.getPrepareRequestWithHeight(from, s.currHeight+1, hashes...) +func (s testState) getPrepareRequest(from uint16, txs ...dbft.Transaction[crypto.Uint256]) Payload { + return s.getPrepareRequestWithHeight(from, s.currHeight+1, txs...) +} + +func (s testState) getPrepareRequestWithMissing(from uint16, txs []dbft.Transaction[crypto.Uint256], missing ...int) Payload { + req := consensus.NewPrepareRequestWithMissing(0, 0, txs, missing...) + + p := consensus.NewConsensusPayload(dbft.PrepareRequestType, s.currHeight+1, from, 0, req) + return p } -func (s testState) getPrepareRequestWithHeight(from uint16, height uint32, hashes ...crypto.Uint256) Payload { - req := consensus.NewPrepareRequest(0, 0, hashes) +func (s testState) getPrepareRequestWithHeight(from uint16, height uint32, txs ...dbft.Transaction[crypto.Uint256]) Payload { + req := consensus.NewPrepareRequest(0, 0, txs) p := consensus.NewConsensusPayload(dbft.PrepareRequestType, height, from, 0, req) return p @@ -1074,7 +1069,6 @@ func newTestState(myIndex int, count int) *testState { s := &testState{ myIndex: myIndex, count: count, - pool: newTestPool(), } s.privs, s.pubs = getTestValidators(count) @@ -1123,7 +1117,6 @@ func (s testState) copyWithIndex(myIndex int) *testState { pubs: s.pubs, currHeight: s.currHeight, currHash: s.currHash, - pool: newTestPool(), } } @@ -1137,7 +1130,6 @@ func (s *testState) getOptions() []func(*dbft.Config[crypto.Uint256]) { return s.myIndex, s.privs[s.myIndex], s.pubs[s.myIndex] }), dbft.WithBroadcast[crypto.Uint256](func(p Payload) { s.ch = append(s.ch, p) }), - dbft.WithGetTx[crypto.Uint256](s.pool.Get), dbft.WithProcessBlock[crypto.Uint256](func(b dbft.Block[crypto.Uint256]) error { s.blocks = append(s.blocks, b); return nil }), dbft.WithWatchOnly[crypto.Uint256](func() bool { return false }), dbft.WithGetBlock[crypto.Uint256](func(crypto.Uint256) dbft.Block[crypto.Uint256] { return nil }), @@ -1147,7 +1139,7 @@ func (s *testState) getOptions() []func(*dbft.Config[crypto.Uint256]) { dbft.WithTimePerBlock[crypto.Uint256](func() time.Duration { return time.Second * 10 }), - dbft.WithRequestTx[crypto.Uint256](func(...crypto.Uint256) {}), + dbft.WithRequestTx[crypto.Uint256](func(seq iter.Seq[crypto.Uint256]) {}), dbft.WithGetVerified[crypto.Uint256](func() []dbft.Transaction[crypto.Uint256] { return []dbft.Transaction[crypto.Uint256]{} }), dbft.WithNewConsensusPayload[crypto.Uint256](newConsensusPayload), @@ -1197,23 +1189,23 @@ func (s *testState) getAMEVOptions() []func(*dbft.Config[crypto.Uint256]) { } func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { - if ctx.TransactionHashes == nil { + if ctx.Transactions == nil { 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, ctx.Transactions) return block } func newPreBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.PreBlock[crypto.Uint256] { - if ctx.TransactionHashes == nil { + if ctx.Transactions == nil { 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, ctx.Transactions) return pre } func newAMEVBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { - if ctx.TransactionHashes == nil { + if ctx.Transactions == nil { return nil } var data [][]byte @@ -1242,26 +1234,3 @@ func getTestValidators(n int) (privs []dbft.PrivateKey, pubs []dbft.PublicKey) { return } - -func (tx testTx) Hash() (h crypto.Uint256) { - binary.LittleEndian.PutUint64(h[:], uint64(tx)) - return -} - -func newTestPool() *testPool { - return &testPool{ - storage: make(map[crypto.Uint256]testTx), - } -} - -func (p *testPool) Add(tx testTx) { - p.storage[tx.Hash()] = tx -} - -func (p *testPool) Get(h crypto.Uint256) dbft.Transaction[crypto.Uint256] { - if tx, ok := p.storage[h]; ok { - return tx - } - - return nil -} diff --git a/internal/consensus/amev_preBlock.go b/internal/consensus/amev_preBlock.go index e74faa5e3..2e8086414 100644 --- a/internal/consensus/amev_preBlock.go +++ b/internal/consensus/amev_preBlock.go @@ -22,7 +22,7 @@ type preBlock struct { var _ dbft.PreBlock[crypto.Uint256] = new(preBlock) // NewPreBlock returns new preBlock. -func NewPreBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txHashes []crypto.Uint256) dbft.PreBlock[crypto.Uint256] { +func NewPreBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txs []dbft.Transaction[crypto.Uint256]) dbft.PreBlock[crypto.Uint256] { pre := new(preBlock) pre.Timestamp = uint32(timestamp / 1000000000) pre.Index = index @@ -40,7 +40,11 @@ func NewPreBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce // Canary default value. pre.data = 0xff - if len(txHashes) != 0 { + if len(txs) != 0 { + txHashes := make([]crypto.Uint256, len(txs)) + for i := range txs { + txHashes[i] = txs[i].Hash() + } mt := merkle.NewMerkleTree(txHashes...) pre.MerkleRoot = mt.Root().Hash } diff --git a/internal/consensus/block.go b/internal/consensus/block.go index fdff9c1cf..e672eb632 100644 --- a/internal/consensus/block.go +++ b/internal/consensus/block.go @@ -66,7 +66,7 @@ func (b *neoBlock) SetTransactions(txx []dbft.Transaction[crypto.Uint256]) { } // NewBlock returns new block. -func NewBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txHashes []crypto.Uint256) dbft.Block[crypto.Uint256] { +func NewBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uint64, txs []dbft.Transaction[crypto.Uint256]) dbft.Block[crypto.Uint256] { block := new(neoBlock) block.Timestamp = uint32(timestamp / 1000000000) block.base.Index = index @@ -81,8 +81,12 @@ func NewBlock(timestamp uint64, index uint32, prevHash crypto.Uint256, nonce uin block.base.PrevHash = prevHash block.ConsensusData = nonce - if len(txHashes) != 0 { - mt := merkle.NewMerkleTree(txHashes...) + if len(txs) != 0 { + hashes := make([]crypto.Uint256, len(txs)) + for i := range hashes { + hashes[i] = txs[i].Hash() + } + mt := merkle.NewMerkleTree(hashes...) block.base.MerkleRoot = mt.Root().Hash } return block diff --git a/internal/consensus/block_test.go b/internal/consensus/block_test.go index 9937686c9..7b8067615 100644 --- a/internal/consensus/block_test.go +++ b/internal/consensus/block_test.go @@ -2,7 +2,6 @@ package consensus import ( "bytes" - "encoding/binary" "encoding/gob" "errors" "testing" @@ -18,7 +17,7 @@ func TestNeoBlock_Setters(t *testing.T) { require.Equal(t, crypto.Uint256{}, b.Hash()) - txs := []dbft.Transaction[crypto.Uint256]{testTx(1), testTx(2)} + txs := []dbft.Transaction[crypto.Uint256]{new(Tx64(1)), new(Tx64(2))} b.SetTransactions(txs) assert.Equal(t, txs, b.Transactions()) @@ -70,10 +69,3 @@ func (t testKey) UnmarshalBinary([]byte) error { return nil } func (t testKey) Sign([]byte) ([]byte, error) { return nil, errors.New("can't sign") } - -type testTx uint64 - -func (tx testTx) Hash() (h crypto.Uint256) { - binary.LittleEndian.PutUint64(h[:], uint64(tx)) - return -} diff --git a/internal/consensus/consensus.go b/internal/consensus/consensus.go index 459f6c92f..5b46caa6c 100644 --- a/internal/consensus/consensus.go +++ b/internal/consensus/consensus.go @@ -10,7 +10,6 @@ import ( ) func New(logger *zap.Logger, key dbft.PrivateKey, pub dbft.PublicKey, - getTx func(uint256 crypto.Uint256) dbft.Transaction[crypto.Uint256], getVerified func() []dbft.Transaction[crypto.Uint256], broadcast func(dbft.ConsensusPayload[crypto.Uint256]), processBlock func(dbft.Block[crypto.Uint256]) error, @@ -33,7 +32,6 @@ func New(logger *zap.Logger, key dbft.PrivateKey, pub dbft.PublicKey, return -1, nil, nil }), - dbft.WithGetTx[crypto.Uint256](getTx), dbft.WithGetVerified[crypto.Uint256](getVerified), dbft.WithBroadcast[crypto.Uint256](broadcast), dbft.WithProcessBlock[crypto.Uint256](processBlock), @@ -58,10 +56,10 @@ 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 { + if ctx.Transactions == nil { return nil } - block := NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + block := NewBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.Transactions) return block } diff --git a/internal/consensus/constructors.go b/internal/consensus/constructors.go index 9a0636b96..b20d08e9a 100644 --- a/internal/consensus/constructors.go +++ b/internal/consensus/constructors.go @@ -2,6 +2,7 @@ package consensus import ( "encoding/binary" + "slices" "github.com/nspcc-dev/dbft" "github.com/nspcc-dev/dbft/internal/crypto" @@ -21,11 +22,27 @@ func NewConsensusPayload(t dbft.MessageType, height uint32, validatorIndex uint1 } // NewPrepareRequest returns minimal prepareRequest implementation. -func NewPrepareRequest(ts uint64, nonce uint64, transactionHashes []crypto.Uint256) dbft.PrepareRequest[crypto.Uint256] { +func NewPrepareRequest(ts uint64, nonce uint64, txs []dbft.Transaction[crypto.Uint256]) dbft.PrepareRequest[crypto.Uint256] { + return NewPrepareRequestWithMissing(ts, nonce, txs) +} + +// NewPrepareRequestWithMissing returns prepareRequest implementation with the +// specified transactions marked as missing. +func NewPrepareRequestWithMissing(ts uint64, nonce uint64, txs []dbft.Transaction[crypto.Uint256], missing ...int) dbft.PrepareRequest[crypto.Uint256] { + reqTxs := make([]*Tx64, len(txs)) + missingTxs := make(map[crypto.Uint256]int) + for i, tx := range txs { + tx64 := tx.(*Tx64) + reqTxs[i] = tx64 + if slices.Contains(missing, i) { + missingTxs[tx64.Hash()] = i + } + } return &prepareRequest{ - transactionHashes: transactionHashes, - nonce: nonce, - timestamp: nanoSecToSec(ts), + txs: reqTxs, + nonce: nonce, + timestamp: nanoSecToSec(ts), + missing: missingTxs, } } diff --git a/internal/consensus/message_test.go b/internal/consensus/message_test.go index ce634884f..36d3109b0 100644 --- a/internal/consensus/message_test.go +++ b/internal/consensus/message_test.go @@ -21,9 +21,9 @@ func TestPayload_EncodeDecode(t *testing.T) { m := generateMessage(dbft.PrepareRequestType, &prepareRequest{ nonce: 123, timestamp: 345, - transactionHashes: []crypto.Uint256{ - {1, 2, 3}, - {5, 6, 7}, + txs: []*Tx64{ + new(Tx64(1)), + new(Tx64(2)), }, }) @@ -77,9 +77,9 @@ func TestPayload_EncodeDecode(t *testing.T) { prepareRequest: &prepareRequest{ nonce: 123, timestamp: 345, - transactionHashes: []crypto.Uint256{ - {1, 2, 3}, - {5, 6, 7}, + txs: []*Tx64{ + new(Tx64(1)), + new(Tx64(2)), }, }, }) diff --git a/internal/consensus/prepare_request.go b/internal/consensus/prepare_request.go index 4955cb3e3..d3570145a 100644 --- a/internal/consensus/prepare_request.go +++ b/internal/consensus/prepare_request.go @@ -9,15 +9,16 @@ import ( type ( prepareRequest struct { - transactionHashes []crypto.Uint256 - nonce uint64 - timestamp uint32 + txs []*Tx64 + nonce uint64 + timestamp uint32 + missing map[crypto.Uint256]int } // prepareRequestAux is an auxiliary structure for prepareRequest encoding. prepareRequestAux struct { - TransactionHashes []crypto.Uint256 - Nonce uint64 - Timestamp uint32 + Txs []*Tx64 + Nonce uint64 + Timestamp uint32 } ) @@ -26,9 +27,9 @@ var _ dbft.PrepareRequest[crypto.Uint256] = (*prepareRequest)(nil) // EncodeBinary implements Serializable interface. func (p prepareRequest) EncodeBinary(w *gob.Encoder) error { return w.Encode(&prepareRequestAux{ - TransactionHashes: p.transactionHashes, - Nonce: p.nonce, - Timestamp: p.timestamp, + Txs: p.txs, + Nonce: p.nonce, + Timestamp: p.timestamp, }) } @@ -41,7 +42,7 @@ func (p *prepareRequest) DecodeBinary(r *gob.Decoder) error { p.timestamp = aux.Timestamp p.nonce = aux.Nonce - p.transactionHashes = aux.TransactionHashes + p.txs = aux.Txs return nil } @@ -55,7 +56,11 @@ func (p prepareRequest) Nonce() uint64 { return p.nonce } -// TransactionHashes implements PrepareRequest interface. -func (p prepareRequest) TransactionHashes() []crypto.Uint256 { - return p.transactionHashes +// Transactions implements PrepareRequest interface. +func (p prepareRequest) Transactions() ([]dbft.Transaction[crypto.Uint256], map[crypto.Uint256]int) { + txs := make([]dbft.Transaction[crypto.Uint256], len(p.txs)) + for i, tx := range p.txs { + txs[i] = tx + } + return txs, p.missing } diff --git a/internal/consensus/recovery_message.go b/internal/consensus/recovery_message.go index a68557b63..e538faa4d 100644 --- a/internal/consensus/recovery_message.go +++ b/internal/consensus/recovery_message.go @@ -86,11 +86,16 @@ func (m *recoveryMessage) GetPrepareRequest(p dbft.ConsensusPayload[crypto.Uint2 return nil } + txs, _ := m.prepareRequest.Transactions() + reqTxs := make([]*Tx64, len(txs)) + for i, tx := range txs { + reqTxs[i] = tx.(*Tx64) + } req := fromPayload(dbft.PrepareRequestType, p, &prepareRequest{ // 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(), + timestamp: nanoSecToSec(m.prepareRequest.Timestamp()), + nonce: m.prepareRequest.Nonce(), + txs: reqTxs, }) req.SetValidatorIndex(ind) diff --git a/internal/simulation/main.go b/internal/simulation/main.go index 62bc617c2..5c48a02f5 100644 --- a/internal/simulation/main.go +++ b/internal/simulation/main.go @@ -114,7 +114,7 @@ func initSimNode(nodes []*simNode, i int, log *zap.Logger) error { } var err error - nodes[i].d, err = consensus.New(nodes[i].log, key, pub, nodes[i].pool.Get, + nodes[i].d, err = consensus.New(nodes[i].log, key, pub, nodes[i].pool.GetVerified, nodes[i].Broadcast, nodes[i].ProcessBlock, diff --git a/prepare_request.go b/prepare_request.go index 3ba594c9c..1f5b53712 100644 --- a/prepare_request.go +++ b/prepare_request.go @@ -6,6 +6,8 @@ type PrepareRequest[H Hash] interface { Timestamp() uint64 // Nonce is a random nonce. Nonce() uint64 - // TransactionHashes returns hashes of all transaction in a proposed block. - TransactionHashes() []H + // Transactions returns the list of all transactions in a proposed block + // with possible gaps in place of missing transactions and the map of + // missing transaction hashes to their indexes in the proposal list. + Transactions() ([]Transaction[H], map[H]int) } diff --git a/send.go b/send.go index 6c40d952e..189029723 100644 --- a/send.go +++ b/send.go @@ -21,7 +21,7 @@ func (c *Context[H]) makePrepareRequest(force bool) ConsensusPayload[H] { return nil } - req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.TransactionHashes) + req := c.Config.NewPrepareRequest(c.Timestamp, c.Nonce, c.Transactions) return c.Config.NewConsensusPayload(c, PrepareRequestType, req) }