Skip to content

Commit

Permalink
WIP: add testing infrastructure for Anti-MEV dBFT extension
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
AnnaShaleva committed Jul 17, 2024
1 parent c2f52d3 commit 7d7004b
Show file tree
Hide file tree
Showing 10 changed files with 439 additions and 5 deletions.
4 changes: 2 additions & 2 deletions check.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Check warning on line 308 in context.go

View check run for this annotation

Codecov / codecov/patch

context.go#L308

Added line #L308 was not covered by tests
}

txx := make([]Transaction[H], len(c.TransactionHashes))

for i, h := range c.TransactionHashes {
Expand Down
6 changes: 6 additions & 0 deletions dbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,3 +716,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
}
115 changes: 115 additions & 0 deletions dbft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -744,6 +745,67 @@ func TestDBFT_FourGoodNodesDeadlock(t *testing.T) {
require.NotNil(t, r1.nextBlock())
}

func TestDBFT_OnReceiveCommitAck(t *testing.T) {
s := newTestState(2, 4)
t.Run("send commit 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.CommitType, 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.GetCommit().Signature()))

t.Run("send commitAck after enough commits", func(t *testing.T) {
s0 := s.copyWithIndex(0)
require.NoError(t, service.PreHeader().SetData(s0.privs[0]))
c0 := s0.getAMEVCommit(0, service.PreHeader().Data())
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.PreHeader().SetData(s1.privs[1]))
c1 := s1.getAMEVCommit(1, service.PreHeader().Data())
service.OnReceive(c1)

// 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.
// require.Equal(t, s.currHeight+1, b.Index())
b := s.nextPreBlock()
require.NotNil(t, b)
require.Nil(t, s.nextBlock())

cmAck := s.tryRecv()
require.NotNil(t, cmAck)
require.Equal(t, dbft.CommitAckType, cmAck.Type())
require.EqualValues(t, s.currHeight+1, cmAck.Height())
require.EqualValues(t, 0, cmAck.ViewNumber())
require.EqualValues(t, s.myIndex, cmAck.ValidatorIndex())
require.NotNil(t, cmAck.Payload())
})
})
}

func (s testState) getChangeView(from uint16, view byte) Payload {
cv := consensus.NewChangeView(view, 0, 0)

Expand All @@ -762,6 +824,12 @@ 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) getPrepareResponse(from uint16, phash crypto.Uint256) Payload {
resp := consensus.NewPrepareResponse(phash)

Expand Down Expand Up @@ -814,6 +882,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,
Expand Down Expand Up @@ -875,6 +954,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.WithNewCommit[crypto.Uint256](consensus.NewAMEVCommit),
dbft.WithNewCommitAck[crypto.Uint256](consensus.NewCommitAck),
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
Expand All @@ -883,6 +976,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] {
Expand Down
126 changes: 126 additions & 0 deletions internal/consensus/amev_block.go
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 27 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L24-L27

Added lines #L24 - L27 were not covered by tests

// 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])

Check warning on line 34 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L32-L34

Added lines #L32 - L34 were not covered by tests
}
tx := Tx64(math.MaxInt64 - int64(sum))
res.transactions = append(preB.initialTransactions, &tx)

Check warning on line 37 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L36-L37

Added lines #L36 - L37 were not covered by tests

// 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()

Check warning on line 42 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L40-L42

Added lines #L40 - L42 were not covered by tests
}
mt := merkle.NewMerkleTree(txHashes...)
res.base.MerkleRoot = mt.Root().Hash

Check warning on line 45 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L44-L45

Added lines #L44 - L45 were not covered by tests

return res

Check warning on line 47 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L47

Added line #L47 was not covered by tests
}

// PrevHash implements Block interface.
func (b *amevBlock) PrevHash() crypto.Uint256 {
return b.base.PrevHash

Check warning on line 52 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L51-L52

Added lines #L51 - L52 were not covered by tests
}

// Index implements Block interface.
func (b *amevBlock) Index() uint32 {
return b.base.Index

Check warning on line 57 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L56-L57

Added lines #L56 - L57 were not covered by tests
}

// MerkleRoot implements Block interface.
func (b *amevBlock) MerkleRoot() crypto.Uint256 {
return b.base.MerkleRoot

Check warning on line 62 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L61-L62

Added lines #L61 - L62 were not covered by tests
}

// Transactions implements Block interface.
func (b *amevBlock) Transactions() []dbft.Transaction[crypto.Uint256] {
return b.transactions

Check warning on line 67 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L66-L67

Added lines #L66 - L67 were not covered by tests
}

// 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]) {

Check warning on line 72 in internal/consensus/amev_block.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'txx' seems to be unused, consider removing or renaming it as _ (revive)
panic("MUST NOT BE CALLED BY DBFT")

Check warning on line 73 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L72-L73

Added lines #L72 - L73 were not covered by tests
}

// Signature implements Block interface.
func (b *amevBlock) Signature() []byte {
return b.signature

Check warning on line 78 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L77-L78

Added lines #L77 - L78 were not covered by tests
}

// 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)

Check warning on line 89 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L86-L89

Added lines #L86 - L89 were not covered by tests

return buf.Bytes()

Check warning on line 91 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L91

Added line #L91 was not covered by tests
}

// Sign implements Block interface.
func (b *amevBlock) Sign(key dbft.PrivateKey) error {
data := b.GetHashData()

Check warning on line 96 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L95-L96

Added lines #L95 - L96 were not covered by tests

sign, err := key.Sign(data)
if err != nil {
return err

Check warning on line 100 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L98-L100

Added lines #L98 - L100 were not covered by tests
}

b.signature = sign

Check warning on line 103 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L103

Added line #L103 was not covered by tests

return nil

Check warning on line 105 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L105

Added line #L105 was not covered by tests
}

// Verify implements Block interface.
func (b *amevBlock) Verify(pub dbft.PublicKey, sign []byte) error {
data := b.GetHashData()
return pub.(*crypto.ECDSAPub).Verify(data, sign)

Check warning on line 111 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L109-L111

Added lines #L109 - L111 were not covered by tests
}

// Hash implements Block interface.
func (b *amevBlock) Hash() (h crypto.Uint256) {
if b.hash != nil {
return *b.hash
} else if b.transactions == nil {
return

Check warning on line 119 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L115-L119

Added lines #L115 - L119 were not covered by tests
}

hash := crypto.Hash256(b.GetHashData())
b.hash = &hash

Check warning on line 123 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L122-L123

Added lines #L122 - L123 were not covered by tests

return hash

Check warning on line 125 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L125

Added line #L125 was not covered by tests
}
44 changes: 44 additions & 0 deletions internal/consensus/amev_commit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package consensus

import (
"encoding/binary"
"encoding/gob"

"github.com/nspcc-dev/dbft"
)

type (
amevCommit struct {
magic uint32 // some magic data CN have to exchange to properly construct final amevBlock.
}
// commitAux is an auxiliary structure for commit encoding.
amevCommitAux struct {
Magic uint32
}
)

var _ dbft.Commit = (*amevCommit)(nil)

// EncodeBinary implements Serializable interface.
func (c amevCommit) EncodeBinary(w *gob.Encoder) error {
return w.Encode(amevCommitAux{
Magic: c.magic,
})

Check warning on line 26 in internal/consensus/amev_commit.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L23-L26

Added lines #L23 - L26 were not covered by tests
}

// DecodeBinary implements Serializable interface.
func (c *amevCommit) DecodeBinary(r *gob.Decoder) error {
aux := new(amevCommitAux)
if err := r.Decode(aux); err != nil {
return err

Check warning on line 33 in internal/consensus/amev_commit.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L30-L33

Added lines #L30 - L33 were not covered by tests
}
c.magic = aux.Magic
return nil

Check warning on line 36 in internal/consensus/amev_commit.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L35-L36

Added lines #L35 - L36 were not covered by tests
}

// Signature implements Commit interface.
func (c amevCommit) Signature() []byte {
res := make([]byte, 4)
binary.BigEndian.PutUint32(res, c.magic)
return res

Check warning on line 43 in internal/consensus/amev_commit.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L40-L43

Added lines #L40 - L43 were not covered by tests
}
Loading

0 comments on commit 7d7004b

Please sign in to comment.