Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow min commitment of single state sync event #1475

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
}