Skip to content

Commit

Permalink
Allow min commitment of single state sync event (#1475)
Browse files Browse the repository at this point in the history
* Allow min commitment of single state sync event

* Fix assertion in checkStateSyncResultLogs

* Change assertions so that multiple deposit batches test can be run independently

* Remove unnecesary cast

* Add UT

* Reorganize files

* Fix UT

* Remove constant

* Fix

---------

Co-authored-by: Goran Rojovic <goran.rojovic@ethernal.tech>
  • Loading branch information
Stefan-Ethernal and goran-ethernal committed May 24, 2023
1 parent 96d0d89 commit 7890d28
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 391 deletions.
202 changes: 202 additions & 0 deletions consensus/polybft/state_sync_commitment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package polybft

import (
"bytes"
"errors"
"fmt"

"github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi"
"github.com/0xPolygon/polygon-edge/crypto"
"github.com/0xPolygon/polygon-edge/merkle-tree"
"github.com/0xPolygon/polygon-edge/state/runtime/precompiled"
"github.com/0xPolygon/polygon-edge/types"
)

const (
stTypeBridgeCommitment = "commitment"
stTypeEndEpoch = "end-epoch"
)

// PendingCommitment holds merkle trie of bridge transactions accompanied by epoch number
type PendingCommitment struct {
*contractsapi.StateSyncCommitment
MerkleTree *merkle.MerkleTree
Epoch uint64
}

// NewPendingCommitment creates a new commitment object
func NewPendingCommitment(epoch uint64, stateSyncEvents []*contractsapi.StateSyncedEvent) (*PendingCommitment, error) {
tree, err := createMerkleTree(stateSyncEvents)
if err != nil {
return nil, err
}

return &PendingCommitment{
MerkleTree: tree,
Epoch: epoch,
StateSyncCommitment: &contractsapi.StateSyncCommitment{
StartID: stateSyncEvents[0].ID,
EndID: stateSyncEvents[len(stateSyncEvents)-1].ID,
Root: tree.Hash(),
},
}, nil
}

// Hash calculates hash value for commitment object.
func (cm *PendingCommitment) Hash() (types.Hash, error) {
data, err := cm.StateSyncCommitment.EncodeAbi()
if err != nil {
return types.Hash{}, err
}

return crypto.Keccak256Hash(data), nil
}

var _ contractsapi.StateTransactionInput = &CommitmentMessageSigned{}

// CommitmentMessageSigned encapsulates commitment message with aggregated signatures
type CommitmentMessageSigned struct {
Message *contractsapi.StateSyncCommitment
AggSignature Signature
PublicKeys [][]byte
}

// Hash calculates hash value for commitment object.
func (cm *CommitmentMessageSigned) Hash() (types.Hash, error) {
data, err := cm.Message.EncodeAbi()
if err != nil {
return types.Hash{}, err
}

return crypto.Keccak256Hash(data), nil
}

// VerifyStateSyncProof validates given state sync proof
// against merkle tree root hash contained in the CommitmentMessage
func (cm *CommitmentMessageSigned) VerifyStateSyncProof(proof []types.Hash,
stateSync *contractsapi.StateSyncedEvent) error {
if stateSync == nil {
return errors.New("no state sync event")
}

if stateSync.ID.Uint64() < cm.Message.StartID.Uint64() ||
stateSync.ID.Uint64() > cm.Message.EndID.Uint64() {
return errors.New("invalid state sync ID")
}

hash, err := stateSync.EncodeAbi()
if err != nil {
return err
}

return merkle.VerifyProof(stateSync.ID.Uint64()-cm.Message.StartID.Uint64(),
hash, proof, cm.Message.Root)
}

// ContainsStateSync checks if commitment contains given state sync event
func (cm *CommitmentMessageSigned) ContainsStateSync(stateSyncID uint64) bool {
return cm.Message.StartID.Uint64() <= stateSyncID && cm.Message.EndID.Uint64() >= stateSyncID
}

// EncodeAbi contains logic for encoding arbitrary data into ABI format
func (cm *CommitmentMessageSigned) EncodeAbi() ([]byte, error) {
blsVerificationPart, err := precompiled.BlsVerificationABIType.Encode(
[2]interface{}{cm.PublicKeys, cm.AggSignature.Bitmap})
if err != nil {
return nil, err
}

commit := &contractsapi.CommitStateReceiverFn{
Commitment: cm.Message,
Signature: cm.AggSignature.AggregatedSignature,
Bitmap: blsVerificationPart,
}

return commit.EncodeAbi()
}

// DecodeAbi contains logic for decoding given ABI data
func (cm *CommitmentMessageSigned) DecodeAbi(txData []byte) error {
if len(txData) < abiMethodIDLength {
return fmt.Errorf("invalid commitment data, len = %d", len(txData))
}

commit := contractsapi.CommitStateReceiverFn{}

err := commit.DecodeAbi(txData)
if err != nil {
return err
}

decoded, err := precompiled.BlsVerificationABIType.Decode(commit.Bitmap)
if err != nil {
return err
}

blsMap, isOk := decoded.(map[string]interface{})
if !isOk {
return fmt.Errorf("invalid commitment data. Bls verification part not in correct format")
}

publicKeys, isOk := blsMap["0"].([][]byte)
if !isOk {
return fmt.Errorf("invalid commitment data. Could not find public keys part")
}

bitmap, isOk := blsMap["1"].([]byte)
if !isOk {
return fmt.Errorf("invalid commitment data. Could not find bitmap part")
}

*cm = CommitmentMessageSigned{
Message: commit.Commitment,
AggSignature: Signature{
AggregatedSignature: commit.Signature,
Bitmap: bitmap,
},
PublicKeys: publicKeys,
}

return nil
}

// getCommitmentMessageSignedTx returns a CommitmentMessageSigned object from a commit state transaction
func getCommitmentMessageSignedTx(txs []*types.Transaction) (*CommitmentMessageSigned, error) {
var commitFn contractsapi.CommitStateReceiverFn
for _, tx := range txs {
// skip non state CommitmentMessageSigned transactions
if tx.Type != types.StateTx ||
len(tx.Input) < abiMethodIDLength ||
!bytes.Equal(tx.Input[:abiMethodIDLength], commitFn.Sig()) {
continue
}

obj := &CommitmentMessageSigned{}

if err := obj.DecodeAbi(tx.Input); err != nil {
return nil, fmt.Errorf("get commitment message signed tx error: %w", err)
}

return obj, nil
}

return nil, nil
}

// createMerkleTree creates a merkle tree from provided state sync events
// if only one state sync event is provided, a second, empty leaf will be added to merkle tree
// so that we can have a commitment with a single state sync event
func createMerkleTree(stateSyncEvents []*contractsapi.StateSyncedEvent) (*merkle.MerkleTree, error) {
stateSyncData := make([][]byte, len(stateSyncEvents))

for i, sse := range stateSyncEvents {
data, err := sse.EncodeAbi()
if err != nil {
return nil, err
}

stateSyncData[i] = data
}

return merkle.NewMerkleTree(stateSyncData)
}
178 changes: 178 additions & 0 deletions consensus/polybft/state_sync_commitment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package polybft

import (
"math/big"
"testing"

"github.com/0xPolygon/polygon-edge/consensus/polybft/contractsapi"
"github.com/0xPolygon/polygon-edge/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCommitmentMessage_Hash(t *testing.T) {
t.Parallel()

const (
eventsCount = 10
)

stateSyncEvents := generateStateSyncEvents(t, eventsCount, 0)

trie1, err := createMerkleTree(stateSyncEvents)
require.NoError(t, err)

trie2, err := createMerkleTree(stateSyncEvents[0 : len(stateSyncEvents)-1])
require.NoError(t, err)

commitmentMessage1 := newTestCommitmentSigned(t, trie1.Hash(), 2, 8)
commitmentMessage2 := newTestCommitmentSigned(t, trie1.Hash(), 2, 8)
commitmentMessage3 := newTestCommitmentSigned(t, trie1.Hash(), 6, 10)
commitmentMessage4 := newTestCommitmentSigned(t, trie2.Hash(), 2, 8)

hash1, err := commitmentMessage1.Hash()
require.NoError(t, err)
hash2, err := commitmentMessage2.Hash()
require.NoError(t, err)
hash3, err := commitmentMessage3.Hash()
require.NoError(t, err)
hash4, err := commitmentMessage4.Hash()
require.NoError(t, err)

require.Equal(t, hash1, hash2)
require.NotEqual(t, hash1, hash3)
require.NotEqual(t, hash1, hash4)
require.NotEqual(t, hash3, hash4)
}

func TestCommitmentMessage_ToRegisterCommitmentInputData(t *testing.T) {
t.Parallel()

const epoch, eventsCount = uint64(100), 11
pendingCommitment, _, _ := buildCommitmentAndStateSyncs(t, eventsCount, epoch, uint64(2))
expectedSignedCommitmentMsg := &CommitmentMessageSigned{
Message: pendingCommitment.StateSyncCommitment,
AggSignature: Signature{
Bitmap: []byte{5, 1},
AggregatedSignature: []byte{1, 1},
},
PublicKeys: [][]byte{{0, 1}, {2, 3}, {4, 5}},
}
inputData, err := expectedSignedCommitmentMsg.EncodeAbi()
require.NoError(t, err)
require.NotEmpty(t, inputData)

var actualSignedCommitmentMsg CommitmentMessageSigned

require.NoError(t, actualSignedCommitmentMsg.DecodeAbi(inputData))
require.NoError(t, err)
require.Equal(t, *expectedSignedCommitmentMsg.Message, *actualSignedCommitmentMsg.Message)
require.Equal(t, expectedSignedCommitmentMsg.AggSignature, actualSignedCommitmentMsg.AggSignature)
}

func TestCommitmentMessage_VerifyProof(t *testing.T) {
t.Parallel()

const epoch, eventsCount = uint64(100), 11
commitment, commitmentSigned, stateSyncs := buildCommitmentAndStateSyncs(t, eventsCount, epoch, 0)
require.Equal(t, uint64(10), commitment.EndID.Sub(commitment.EndID, commitment.StartID).Uint64())

for _, stateSync := range stateSyncs {
leaf, err := stateSync.EncodeAbi()
require.NoError(t, err)

proof, err := commitment.MerkleTree.GenerateProof(leaf)
require.NoError(t, err)

execute := &contractsapi.ExecuteStateReceiverFn{
Proof: proof,
Obj: (*contractsapi.StateSync)(stateSync),
}

inputData, err := execute.EncodeAbi()
require.NoError(t, err)

executionStateSync := &contractsapi.ExecuteStateReceiverFn{}
require.NoError(t, executionStateSync.DecodeAbi(inputData))
require.Equal(t, stateSync.ID.Uint64(), executionStateSync.Obj.ID.Uint64())
require.Equal(t, stateSync.Sender, executionStateSync.Obj.Sender)
require.Equal(t, stateSync.Receiver, executionStateSync.Obj.Receiver)
require.Equal(t, stateSync.Data, executionStateSync.Obj.Data)
require.Equal(t, proof, executionStateSync.Proof)

err = commitmentSigned.VerifyStateSyncProof(executionStateSync.Proof,
(*contractsapi.StateSyncedEvent)(executionStateSync.Obj))
require.NoError(t, err)
}
}

func TestCommitmentMessage_VerifyProof_NoStateSyncsInCommitment(t *testing.T) {
t.Parallel()

commitment := &CommitmentMessageSigned{Message: &contractsapi.StateSyncCommitment{StartID: big.NewInt(1), EndID: big.NewInt(10)}}
err := commitment.VerifyStateSyncProof([]types.Hash{}, nil)
assert.ErrorContains(t, err, "no state sync event")
}

func TestCommitmentMessage_VerifyProof_StateSyncHashNotEqualToProof(t *testing.T) {
t.Parallel()

const (
fromIndex = 0
toIndex = 4
)

stateSyncs := generateStateSyncEvents(t, 5, 0)
tree, err := createMerkleTree(stateSyncs)
require.NoError(t, err)

leaf, err := stateSyncs[0].EncodeAbi()
require.NoError(t, err)

proof, err := tree.GenerateProof(leaf)
require.NoError(t, err)

commitment := &CommitmentMessageSigned{
Message: &contractsapi.StateSyncCommitment{
StartID: big.NewInt(fromIndex),
EndID: big.NewInt(toIndex),
Root: tree.Hash(),
},
}

assert.ErrorContains(t, commitment.VerifyStateSyncProof(proof, stateSyncs[4]), "not a member of merkle tree")
}

func newTestCommitmentSigned(t *testing.T, root types.Hash, startID, endID int64) *CommitmentMessageSigned {
t.Helper()

return &CommitmentMessageSigned{
Message: &contractsapi.StateSyncCommitment{
StartID: big.NewInt(startID),
EndID: big.NewInt(endID),
Root: root,
},
AggSignature: Signature{},
PublicKeys: [][]byte{},
}
}

func buildCommitmentAndStateSyncs(t *testing.T, stateSyncsCount int,
epoch, startIdx uint64) (*PendingCommitment, *CommitmentMessageSigned, []*contractsapi.StateSyncedEvent) {
t.Helper()

stateSyncEvents := generateStateSyncEvents(t, stateSyncsCount, startIdx)
commitment, err := NewPendingCommitment(epoch, stateSyncEvents)
require.NoError(t, err)

commitmentSigned := &CommitmentMessageSigned{
Message: commitment.StateSyncCommitment,
AggSignature: Signature{
AggregatedSignature: []byte{},
Bitmap: []byte{},
},
PublicKeys: [][]byte{},
}

return commitment, commitmentSigned, stateSyncEvents
}

0 comments on commit 7890d28

Please sign in to comment.