From 659de523f1e35c3f6733eeb8d431c819ed85a25a Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Wed, 17 Jul 2024 18:43:34 +0300 Subject: [PATCH] WIP: add testing infrastructure for Anti-MEV dBFT extension Add custom PreBlock and Block interfaces implementation, custom Commit and CommitAck, adjust testing logic. WIP, not finished, not buildable, but the idea can be traced. Continue testing infrastructure finalisation. Signed-off-by: Anna Shaleva --- context.go | 4 + dbft.go | 6 ++ dbft_test.go | 142 +++++++++++++++++++++++++++ internal/consensus/amev_block.go | 126 ++++++++++++++++++++++++ internal/consensus/amev_commit.go | 44 +++++++++ internal/consensus/amev_preBlock.go | 76 ++++++++++++++ internal/consensus/amev_preCommit.go | 45 +++++++++ internal/consensus/constructors.go | 16 +++ 8 files changed, 459 insertions(+) create mode 100644 internal/consensus/amev_block.go create mode 100644 internal/consensus/amev_commit.go create mode 100644 internal/consensus/amev_preBlock.go create mode 100644 internal/consensus/amev_preCommit.go 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{