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 18, 2024
1 parent b7f4cc0 commit 659de52
Show file tree
Hide file tree
Showing 8 changed files with 459 additions and 0 deletions.
4 changes: 4 additions & 0 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions dbft.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
142 changes: 142 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,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)

Expand All @@ -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)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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] {
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 transactions from this method must not be
// reused to fill final Block's transactions.
func (b *amevBlock) SetTransactions(txx []dbft.Transaction[crypto.Uint256]) {

Check warning on line 73 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)

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#L73

Added line #L73 was 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/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,
})

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L25-L28

Added lines #L25 - L28 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 35 in internal/consensus/amev_commit.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L32-L35

Added lines #L32 - L35 were not covered by tests
}
c.data = aux.Data
return nil

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_commit.go#L37-L38

Added lines #L37 - L38 were not covered by tests
}

// Signature implements Commit interface.
func (c amevCommit) Signature() []byte {
return c.data[:]

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#L42-L43

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

0 comments on commit 659de52

Please sign in to comment.