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 47b10ab
Show file tree
Hide file tree
Showing 7 changed files with 321 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()

Check warning on line 65 in check.go

View check run for this annotation

Codecov / codecov/patch

check.go#L65

Added line #L65 was not covered by tests
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),

Check warning on line 69 in check.go

View check run for this annotation

Codecov / codecov/patch

check.go#L68-L69

Added lines #L68 - L69 were not covered by tests
zap.Stringer("preBlock hash", hash),
//zap.Stringer("preBlock hash", hash),
zap.Int("tx_count", len(d.preBlock.Transactions())))

Check warning on line 71 in check.go

View check run for this annotation

Codecov / codecov/patch

check.go#L71

Added line #L71 was not covered by tests

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

Check warning on line 214 in context.go

View check run for this annotation

Codecov / codecov/patch

context.go#L213-L214

Added lines #L213 - L214 were not covered by tests
}

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
65 changes: 65 additions & 0 deletions dbft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Check warning on line 750 in dbft_test.go

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 't' seems to be unused, consider removing or renaming it as _ (revive)
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)

Expand Down Expand Up @@ -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
Expand All @@ -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] {
Expand Down
193 changes: 193 additions & 0 deletions internal/consensus/amev_block.go
Original file line number Diff line number Diff line change
@@ -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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L38-L41

Added lines #L38 - L41 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L47-L48

Added lines #L47 - L48 were not covered by tests

pre.base.PrevHash = prevHash
pre.base.ConsensusData = nonce

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L50-L51

Added lines #L50 - L51 were not covered by tests

if len(txHashes) != 0 {
mt := merkle.NewMerkleTree(txHashes...)
pre.base.MerkleRoot = mt.Root().Hash

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L53-L55

Added lines #L53 - L55 were not covered by tests
}
return pre

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

Added line #L57 was not covered by tests
}

func (pre *preBlock) Data() []byte {
var res = make([]byte, 4)
binary.BigEndian.PutUint32(res, pre.data)
return res

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L60-L63

Added lines #L60 - L63 were not covered by tests
}

func (pre *preBlock) SetData(key dbft.PrivateKey) error {

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

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'key' seems to be unused, consider removing or renaming it as _ (revive)
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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L66-L68

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

func (pre *preBlock) Verify(key dbft.PublicKey, data []byte) error {

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

View workflow job for this annotation

GitHub Actions / Lint

unused-parameter: parameter 'key' seems to be unused, consider removing or renaming it as _ (revive)
if len(data) != 4 {
return errors.New("invalid data len")

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

Added lines #L71 - L73 were not covered by tests
}
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")

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L75-L76

Added lines #L75 - L76 were not covered by tests
}
return nil

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

Added line #L78 was not covered by tests
}

func (pre *preBlock) Transactions() []dbft.Transaction[crypto.Uint256] {
return pre.initialTransactions

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L81-L82

Added lines #L81 - L82 were not covered by tests
}

func (pre *preBlock) SetTransactions(txs []dbft.Transaction[crypto.Uint256]) {
pre.initialTransactions = txs

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L85-L86

Added lines #L85 - L86 were not covered by tests
}

// 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 94 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L91-L94

Added lines #L91 - L94 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 101 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L99-L101

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L103-L104

Added lines #L103 - L104 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 109 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L107-L109

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L111-L112

Added lines #L111 - L112 were not covered by tests

return res

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L114

Added line #L114 was not covered by tests
}

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

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#L118-L119

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

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L123-L124

Added lines #L123 - L124 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L128-L129

Added lines #L128 - L129 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L133-L134

Added lines #L133 - L134 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 139 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 140 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L139-L140

Added lines #L139 - L140 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L144-L145

Added lines #L144 - L145 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 156 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L153-L156

Added lines #L153 - L156 were not covered by tests

return buf.Bytes()

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L158

Added line #L158 was not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L162-L163

Added lines #L162 - L163 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L165-L167

Added lines #L165 - L167 were not covered by tests
}

b.signature = sign

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L170

Added line #L170 was not covered by tests

return nil

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L172

Added line #L172 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 178 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L176-L178

Added lines #L176 - L178 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 186 in internal/consensus/amev_block.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L182-L186

Added lines #L182 - L186 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L189-L190

Added lines #L189 - L190 were not covered by tests

return hash

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/amev_block.go#L192

Added line #L192 was not covered by tests
}
44 changes: 44 additions & 0 deletions internal/consensus/commitAck.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 (
// 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,
})

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/commitAck.go#L25-L28

Added lines #L25 - L28 were not covered by tests
}

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

Check warning on line 35 in internal/consensus/commitAck.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/commitAck.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/commitAck.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/commitAck.go#L37-L38

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

// Data implements CommitAck interface.
func (c commitAck) Data() []byte {
return c.data[:]

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

View check run for this annotation

Codecov / codecov/patch

internal/consensus/commitAck.go#L42-L43

Added lines #L42 - L43 were not covered by tests
}
7 changes: 7 additions & 0 deletions internal/consensus/constructors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Check warning on line 56 in internal/consensus/constructors.go

View check run for this annotation

Codecov / codecov/patch

internal/consensus/constructors.go#L53-L56

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

// NewRecoveryRequest returns minimal RecoveryRequest implementation.
func NewRecoveryRequest(ts uint64) dbft.RecoveryRequest {
return &recoveryRequest{
Expand Down
3 changes: 0 additions & 3 deletions pre_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 47b10ab

Please sign in to comment.