diff --git a/context.go b/context.go index 79cb19ed..897780b5 100644 --- a/context.go +++ b/context.go @@ -212,6 +212,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 diff --git a/dbft.go b/dbft.go index 2f2950de..9dc0c9f0 100644 --- a/dbft.go +++ b/dbft.go @@ -707,3 +707,9 @@ func (d *DBFT[H]) extendTimer(count time.Duration) { func (d *DBFT[H]) Header() Block[H] { return d.header } + +// PreHeader returns current preHeader from context. May be nil in case if no +// preHeader is constructed yet. Do not change the resulting preHeader. +func (d *DBFT[H]) PreHeader() PreBlock[H] { + return d.preHeader +} diff --git a/dbft_test.go b/dbft_test.go index adb5cb14..ff956e9a 100644 --- a/dbft_test.go +++ b/dbft_test.go @@ -25,6 +25,7 @@ type testState struct { 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 } @@ -744,6 +745,88 @@ func TestDBFT_FourGoodNodesDeadlock(t *testing.T) { require.NotNil(t, r1.nextBlock()) } +func TestDBFT_OnReceiveCommitAMEV(t *testing.T) { + s := newTestState(2, 4) + t.Run("send preCommit after enough responses", func(t *testing.T) { + s.currHeight = 1 + service, _ := dbft.New[crypto.Uint256](s.getAMEVOptions()...) + 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.PreCommitType, cm.Type()) + require.EqualValues(t, s.currHeight+1, cm.Height()) + require.EqualValues(t, 0, cm.ViewNumber()) + require.EqualValues(t, s.myIndex, cm.ValidatorIndex()) + require.NotNil(t, cm.Payload()) + + pub := s.pubs[s.myIndex] + require.NoError(t, service.PreHeader().Verify(pub, cm.GetPreCommit().Data())) + + t.Run("send commit after enough preCommits", func(t *testing.T) { + s0 := s.copyWithIndex(0) + require.NoError(t, service.PreHeader().SetData(s0.privs[0])) + preC0 := s0.getPreCommit(0, service.PreHeader().Data()) + service.OnReceive(preC0) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextPreBlock()) + require.Nil(t, s.nextBlock()) + + s1 := s.copyWithIndex(1) + require.NoError(t, service.PreHeader().SetData(s1.privs[1])) + preC1 := s1.getPreCommit(1, service.PreHeader().Data()) + service.OnReceive(preC1) + + // TODO: check PreBlock somehow, it doesn't have a lot of public interfaces + // (and it doesn't need them in fact). But for test we need to have an ability + // to ensure it contains expected data. + // require.Equal(t, s.currHeight+1, b.Index()) + b := s.nextPreBlock() + require.NotNil(t, b) + require.Nil(t, s.nextBlock()) + + c := s.tryRecv() + require.NotNil(t, c) + require.Equal(t, dbft.CommitType, c.Type()) + require.EqualValues(t, s.currHeight+1, c.Height()) + require.EqualValues(t, 0, c.ViewNumber()) + require.EqualValues(t, s.myIndex, c.ValidatorIndex()) + require.NotNil(t, c.Payload()) + + t.Run("process block a after enough commitAcks", func(t *testing.T) { + s0 := s.copyWithIndex(0) + require.NoError(t, service.Header().Sign(s0.privs[0])) + c0 := s0.getAMEVCommit(0, service.Header().Signature()) + service.OnReceive(c0) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextPreBlock()) + require.Nil(t, s.nextBlock()) + + s1 := s.copyWithIndex(1) + require.NoError(t, service.Header().Sign(s1.privs[1])) + c1 := s1.getAMEVCommit(1, service.Header().Signature()) + service.OnReceive(c1) + require.Nil(t, s.tryRecv()) + require.Nil(t, s.nextPreBlock()) + + 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) @@ -762,6 +845,18 @@ func (s testState) getCommit(from uint16, sign []byte) Payload { return p } +func (s testState) getAMEVCommit(from uint16, sign []byte) Payload { + c := consensus.NewAMEVCommit(sign) + p := consensus.NewConsensusPayload(dbft.CommitType, s.currHeight+1, from, 0, c) + return p +} + +func (s testState) getPreCommit(from uint16, data []byte) Payload { + c := consensus.NewPreCommit(data) + p := consensus.NewConsensusPayload(dbft.PreCommitType, s.currHeight+1, from, 0, c) + return p +} + func (s testState) getPrepareResponse(from uint16, phash crypto.Uint256) Payload { resp := consensus.NewPrepareResponse(phash) @@ -814,6 +909,17 @@ func (s *testState) nextBlock() dbft.Block[crypto.Uint256] { return b } +func (s *testState) nextPreBlock() dbft.PreBlock[crypto.Uint256] { + if len(s.preBlocks) == 0 { + return nil + } + + b := s.preBlocks[0] + s.preBlocks = s.preBlocks[1:] + + return b +} + func (s testState) copyWithIndex(myIndex int) *testState { return &testState{ myIndex: myIndex, @@ -875,6 +981,20 @@ 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.WithNewPreCommit[crypto.Uint256](consensus.NewPreCommit), + dbft.WithNewCommit[crypto.Uint256](consensus.NewAMEVCommit), + dbft.WithNewPreBlockFromContext[crypto.Uint256](newPreBlockFromContext), + dbft.WithNewBlockFromContext[crypto.Uint256](newAMEVBlockFromContext), + dbft.WithProcessPreBlock(func(b dbft.PreBlock[crypto.Uint256]) { s.preBlocks = append(s.preBlocks, b) }), + ) + + return opts +} + func newBlockFromContext(ctx *dbft.Context[crypto.Uint256]) dbft.Block[crypto.Uint256] { if ctx.TransactionHashes == nil { return nil @@ -883,6 +1003,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.PreCommitPayloads { + if c != nil && c.ViewNumber() == ctx.ViewNumber { + data = append(data, c.GetPreCommit().Data()) + } + } + 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..d7d2cdb9 --- /dev/null +++ b/internal/consensus/amev_block.go @@ -0,0 +1,126 @@ +package consensus + +import ( + "bytes" + "encoding/binary" + "encoding/gob" + "math" + + "github.com/nspcc-dev/dbft" + "github.com/nspcc-dev/dbft/internal/crypto" + "github.com/nspcc-dev/dbft/internal/merkle" +) + +type amevBlock struct { + base + + transactions []dbft.Transaction[crypto.Uint256] + signature []byte + hash *crypto.Uint256 +} + +// 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 transactions from this method must not be +// reused to fill final Block's transactions. +func (b *amevBlock) SetTransactions(txx []dbft.Transaction[crypto.Uint256]) { +} + +// 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/amev_commit.go b/internal/consensus/amev_commit.go new file mode 100644 index 00000000..1b4e509e --- /dev/null +++ b/internal/consensus/amev_commit.go @@ -0,0 +1,44 @@ +package consensus + +import ( + "encoding/gob" + + "github.com/nspcc-dev/dbft" +) + +type ( + // amevCommit implements dbft.Commit. + amevCommit struct { + data [dataSize]byte + } + // amevCommitAux is an auxiliary structure for amevCommit encoding. + amevCommitAux struct { + Data [dataSize]byte + } +) + +const dataSize = 64 + +var _ dbft.Commit = (*amevCommit)(nil) + +// EncodeBinary implements Serializable interface. +func (c amevCommit) EncodeBinary(w *gob.Encoder) error { + return w.Encode(amevCommitAux{ + Data: c.data, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *amevCommit) DecodeBinary(r *gob.Decoder) error { + aux := new(amevCommitAux) + if err := r.Decode(aux); err != nil { + return err + } + c.data = aux.Data + return nil +} + +// Signature implements Commit interface. +func (c amevCommit) Signature() []byte { + return c.data[:] +} diff --git a/internal/consensus/amev_preBlock.go b/internal/consensus/amev_preBlock.go new file mode 100644 index 00000000..dac293c6 --- /dev/null +++ b/internal/consensus/amev_preBlock.go @@ -0,0 +1,76 @@ +package consensus + +import ( + "encoding/binary" + "errors" + + "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] +} + +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 { + // Just an artificial rule for data construction, it can be anything, and in Neo X + // it will be decrypted transactions fragments. + pre.data = pre.base.Index + 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 +} diff --git a/internal/consensus/amev_preCommit.go b/internal/consensus/amev_preCommit.go new file mode 100644 index 00000000..956539f8 --- /dev/null +++ b/internal/consensus/amev_preCommit.go @@ -0,0 +1,45 @@ +package consensus + +import ( + "encoding/binary" + "encoding/gob" + + "github.com/nspcc-dev/dbft" +) + +type ( + // preCommit implements dbft.PreCommit. + preCommit struct { + magic uint32 // some magic data CN have to exchange to properly construct final amevBlock. + } + // preCommitAux is an auxiliary structure for preCommit encoding. + preCommitAux struct { + Magic uint32 + } +) + +var _ dbft.PreCommit = (*preCommit)(nil) + +// EncodeBinary implements Serializable interface. +func (c preCommit) EncodeBinary(w *gob.Encoder) error { + return w.Encode(preCommitAux{ + Magic: c.magic, + }) +} + +// DecodeBinary implements Serializable interface. +func (c *preCommit) DecodeBinary(r *gob.Decoder) error { + aux := new(preCommitAux) + if err := r.Decode(aux); err != nil { + return err + } + c.magic = aux.Magic + return nil +} + +// Data implements PreCommit interface. +func (c preCommit) Data() []byte { + res := make([]byte, 4) + binary.BigEndian.PutUint32(res, c.magic) + return res +} diff --git a/internal/consensus/constructors.go b/internal/consensus/constructors.go index 44326b02..096fa37d 100644 --- a/internal/consensus/constructors.go +++ b/internal/consensus/constructors.go @@ -1,6 +1,8 @@ package consensus import ( + "encoding/binary" + "github.com/nspcc-dev/dbft" "github.com/nspcc-dev/dbft/internal/crypto" ) @@ -49,6 +51,20 @@ func NewCommit(signature []byte) dbft.Commit { return c } +// NewPreCommit returns minimal dbft.PreCommit implementation. +func NewPreCommit(data []byte) dbft.PreCommit { + c := new(preCommit) + c.magic = binary.BigEndian.Uint32(data) + return c +} + +// NewAMEVCommit returns minimal dbft.Commit implementation for anti-MEV extension. +func NewAMEVCommit(data []byte) dbft.Commit { + c := new(amevCommit) + copy(c.data[:], data) + return c +} + // NewRecoveryRequest returns minimal RecoveryRequest implementation. func NewRecoveryRequest(ts uint64) dbft.RecoveryRequest { return &recoveryRequest{