diff --git a/check.go b/check.go index 128ddd21..780ddf2e 100644 --- a/check.go +++ b/check.go @@ -63,11 +63,11 @@ func (d *DBFT[H]) checkCommit() { // signature bytes, as with usual dBFT. if d.isAntiMEVExtensionEnabled() { d.preBlock = d.CreatePreBlock() - hash := d.preBlock.Hash() + // hash := d.preBlock.Hash() // PreBlock Hash is needed only for informational purposes, it doesn't arry any sence and will be changed by CommitAck phase anyway. d.Logger.Info("processing PreBlock", zap.Uint32("height", d.BlockIndex), - zap.Stringer("preBlock hash", hash), + //zap.Stringer("preBlock hash", hash), zap.Int("tx_count", len(d.preBlock.Transactions()))) d.preBlockProcessed = true diff --git a/context.go b/context.go index 81308eb3..9969f017 100644 --- a/context.go +++ b/context.go @@ -210,6 +210,10 @@ func (c *Context[H]) MoreThanFNodesCommittedOrLost() bool { return c.CountCommitted()+c.CountFailed() > c.F() } +func (c *Context[H]) PreBlock() PreBlock[H] { + return c.preHeader // without transactions +} + func (c *Context[H]) reset(view byte, ts uint64) { c.MyIndex = -1 c.lastBlockTimestamp = ts @@ -298,6 +302,12 @@ func (c *Context[H]) CreateBlock() Block[H] { return nil } + // For anti-MEV extensions we don't need to initialize block's transactions + // since we already did this for PreBlock. + if c.isAntiMEVExtensionEnabled() { + return c.block + } + txx := make([]Transaction[H], len(c.TransactionHashes)) for i, h := range c.TransactionHashes { diff --git a/dbft_test.go b/dbft_test.go index adb5cb14..63795d43 100644 --- a/dbft_test.go +++ b/dbft_test.go @@ -744,6 +744,38 @@ func TestDBFT_FourGoodNodesDeadlock(t *testing.T) { require.NotNil(t, r1.nextBlock()) } +func TestDBFT_OnReceiveCommitAck(t *testing.T) { + s := newTestState(2, 4) + + t.Run("send commitAck after enough commits", func(t *testing.T) { + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](append(s.getOptions())...) + service.Start(0) + /* + req := s.tryRecv() + require.NotNil(t, req) + + resp := s.getPrepareResponse(1, req.Hash()) + service.OnReceive(resp) + require.Nil(t, s.tryRecv()) + + resp = s.getPrepareResponse(0, req.Hash()) + service.OnReceive(resp) + + cm := s.tryRecv() + require.NotNil(t, cm) + require.Equal(t, dbft.CommitType, cm.Type()) + require.EqualValues(t, s.currHeight+1, cm.Height()) + require.EqualValues(t, 0, cm.ViewNumber()) + require.Equal(t, s.currHash, cm.PrevHash()) + require.EqualValues(t, s.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + + pub := s.pubs[s.myIndex] + require.NoError(t, service.header.Verify(pub, cm.GetCommit().Signature()))*/ + }) +} + func (s testState) getChangeView(from uint16, view byte) Payload { cv := consensus.NewChangeView(view, 0, 0) @@ -875,6 +907,17 @@ func (s *testState) getOptions() []func(*dbft.Config[crypto.Uint256]) { return opts } +func (s *testState) getAMEVOptions() []func(*dbft.Config[crypto.Uint256]) { + opts := s.getOptions() + opts = append(opts, + dbft.WithAntiMEVExtensionEnablingHeight[crypto.Uint256](0), + dbft.WithNewCommitAck[crypto.Uint256](consensus.NewCommitAck), + dbft.WithNewPreBlockFromContext[crypto.Uint256](newPreBlockFromContext), + ) + + return opts +} + func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { if ctx.TransactionHashes == nil { return nil @@ -883,6 +926,28 @@ func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Ui return block } +func newPreBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.PreBlock[crypto.Uint256] { + if ctx.TransactionHashes == nil { + return nil + } + pre := consensus.NewPreBlock(ctx.Timestamp, ctx.BlockIndex, ctx.PrevHash, ctx.Nonce, ctx.TransactionHashes) + return pre +} + +func newAMEVBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { + if ctx.TransactionHashes == nil { + return nil + } + var data [][]byte + for _, c := range ctx.CommitPayloads { + if c != nil && c.ViewNumber() == ctx.ViewNumber { + data = append(data, c.GetCommit().Signature()) + } + } + pre := consensus.NewAMEVBlock(ctx.PreBlock(), data, ctx.M()) + return pre +} + // newConsensusPayload is a function for creating consensus payload of specific // type. func newConsensusPayload(c *dbft.Context[crypto.Uint256], t dbft.MessageType, msg any) dbft.ConsensusPayload[crypto.Uint256] { diff --git a/internal/consensus/amev_block.go b/internal/consensus/amev_block.go new file mode 100644 index 00000000..4a5d6585 --- /dev/null +++ b/internal/consensus/amev_block.go @@ -0,0 +1,193 @@ +package consensus + +import ( + "bytes" + "encoding/binary" + "encoding/gob" + "errors" + "math" + + "github.com/nspcc-dev/dbft" + "github.com/nspcc-dev/dbft/internal/crypto" + "github.com/nspcc-dev/dbft/internal/merkle" +) + +type ( + preBlock struct { + base + + // A magic number CN nodes should exchange during Commit phase + // and used to construct the final list of transactions for amevBlock. + data uint32 + + initialTransactions []dbft.Transaction[crypto.Uint256] + } + + amevBlock struct { + base + + transactions []dbft.Transaction[crypto.Uint256] + signature []byte + hash *crypto.Uint256 + } +) + +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] { + pre := new(preBlock) + pre.base.Timestamp = uint32(timestamp / 1000000000) + pre.base.Index = index + + // NextConsensus and Version information is not provided by dBFT context, + // these are implementation-specific fields, and thus, should be managed outside the + // dBFT library. For simulation simplicity, let's assume that these fields are filled + // by every CN separately and is not verified. + pre.base.NextConsensus = crypto.Uint160{1, 2, 3} + pre.base.Version = 0 + + pre.base.PrevHash = prevHash + pre.base.ConsensusData = nonce + + if len(txHashes) != 0 { + mt := merkle.NewMerkleTree(txHashes...) + pre.base.MerkleRoot = mt.Root().Hash + } + return pre +} + +func (pre *preBlock) Data() []byte { + var res = make([]byte, 4) + binary.BigEndian.PutUint32(res, pre.data) + return res +} + +func (pre *preBlock) SetData(key dbft.PrivateKey) error { + pre.data = pre.base.Index // Just a custom rule for data, it can be anything, and in Neo X it will be decrypted transactions fragments. + return nil +} + +func (pre *preBlock) Verify(key dbft.PublicKey, data []byte) error { + if len(data) != 4 { + return errors.New("invalid data len") + } + if binary.BigEndian.Uint32(data) != pre.base.Index { // Just an artificial verification rule, and for NeoX it should be decrypted transactions fragments verification. + return errors.New("invalid data") + } + return nil +} + +func (pre *preBlock) Transactions() []dbft.Transaction[crypto.Uint256] { + return pre.initialTransactions +} + +func (pre *preBlock) SetTransactions(txs []dbft.Transaction[crypto.Uint256]) { + pre.initialTransactions = txs +} + +// NewAMEVBlock returns new block based on PreBlock and additional Commit-level data +// collected from M consensus nodes. +func NewAMEVBlock(pre dbft.PreBlock[crypto.Uint256], cnData [][]byte, m int) dbft.Block[crypto.Uint256] { + preB := pre.(*preBlock) + res := new(amevBlock) + res.base = preB.base + + // Based on the provided cnData we'll add one more transaction to the resulting block. + // Some artificial rules of new tx creation are invented here, but in Neo X there will + // be well-defined custom rules for Envelope transactions. + var sum uint32 + for i := 0; i < m; i++ { + sum += binary.BigEndian.Uint32(cnData[i]) + } + tx := Tx64(math.MaxInt64 - int64(sum)) + res.transactions = append(preB.initialTransactions, &tx) + + // Rebuild Merkle root for the new set of transations. + txHashes := make([]crypto.Uint256, len(res.transactions)) + for i := range txHashes { + txHashes[i] = res.transactions[i].Hash() + } + mt := merkle.NewMerkleTree(txHashes...) + res.base.MerkleRoot = mt.Root().Hash + + return res +} + +// PrevHash implements Block interface. +func (b *amevBlock) PrevHash() crypto.Uint256 { + return b.base.PrevHash +} + +// Index implements Block interface. +func (b *amevBlock) Index() uint32 { + return b.base.Index +} + +// MerkleRoot implements Block interface. +func (b *amevBlock) MerkleRoot() crypto.Uint256 { + return b.base.MerkleRoot +} + +// Transactions implements Block interface. +func (b *amevBlock) Transactions() []dbft.Transaction[crypto.Uint256] { + return b.transactions +} + +// SetTransactions implements Block interface. This method is special since it's +// left for dBFT 2.0 compatibility and must not be called for amevBlock. +func (b *amevBlock) SetTransactions(txx []dbft.Transaction[crypto.Uint256]) { + panic("MUST NOT BE CALLED BY DBFT") +} + +// Signature implements Block interface. +func (b *amevBlock) Signature() []byte { + return b.signature +} + +// GetHashData returns data for hashing and signing. +// It must be an injection of the set of blocks to the set +// of byte slices, i.e: +// 1. It must have only one valid result for one block. +// 2. Two different blocks must have different hash data. +func (b *amevBlock) GetHashData() []byte { + buf := bytes.Buffer{} + w := gob.NewEncoder(&buf) + _ = b.EncodeBinary(w) + + return buf.Bytes() +} + +// Sign implements Block interface. +func (b *amevBlock) Sign(key dbft.PrivateKey) error { + data := b.GetHashData() + + sign, err := key.Sign(data) + if err != nil { + return err + } + + b.signature = sign + + return nil +} + +// Verify implements Block interface. +func (b *amevBlock) Verify(pub dbft.PublicKey, sign []byte) error { + data := b.GetHashData() + return pub.(*crypto.ECDSAPub).Verify(data, sign) +} + +// Hash implements Block interface. +func (b *amevBlock) Hash() (h crypto.Uint256) { + if b.hash != nil { + return *b.hash + } else if b.transactions == nil { + return + } + + hash := crypto.Hash256(b.GetHashData()) + b.hash = &hash + + return hash +} diff --git a/internal/consensus/commitAck.go b/internal/consensus/commitAck.go new file mode 100644 index 00000000..c41cc711 --- /dev/null +++ b/internal/consensus/commitAck.go @@ -0,0 +1,44 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/nspcc-dev/dbft" +) + +type ( + // commitAck implements dbft.CommitAck and holds some side data. + commitAck struct { + data [dataSize]byte + } + // commitAckAux is an auxiliary structure for commitAck encoding. + commitAckAux struct { + Data [dataSize]byte + } +) + +const dataSize = 20 + +var _ dbft.CommitAck = (*commitAck)(nil) + +// EncodeBinary implements Serializable interface. +func (c commitAck) EncodeBinary(w *gob.Encoder) error { + return w.Encode(commitAckAux{ + Data: c.data, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *commitAck) DecodeBinary(r *gob.Decoder) error { + aux := new(commitAckAux) + if err := r.Decode(aux); err != nil { + return err + } + c.data = aux.Data + return nil +} + +// Data implements CommitAck interface. +func (c commitAck) Data() []byte { + return c.data[:] +} diff --git a/internal/consensus/constructors.go b/internal/consensus/constructors.go index 44326b02..e9b9ff7d 100644 --- a/internal/consensus/constructors.go +++ b/internal/consensus/constructors.go @@ -49,6 +49,13 @@ func NewCommit(signature []byte) dbft.Commit { return c } +// NewCommitAck returns minimal CommitAck implementation. +func NewCommitAck(data []byte) dbft.CommitAck { + c := new(commitAck) + copy(c.data[:], data) + return c +} + // NewRecoveryRequest returns minimal RecoveryRequest implementation. func NewRecoveryRequest(ts uint64) dbft.RecoveryRequest { return &recoveryRequest{ diff --git a/pre_block.go b/pre_block.go index 2931c38a..b75c5cb4 100644 --- a/pre_block.go +++ b/pre_block.go @@ -2,9 +2,6 @@ package dbft // PreBlock is a generic interface for a preBlock used by anti-MEV dBFT extension. type PreBlock[H Hash] interface { - // Hash returns PreBlock hash. - Hash() H // needed for informational log, but may be removed. - // Data returns PreBlock's data CNs need to exchange during Commit phase. // It's not a final block signature. Data() []byte // required