Skip to content

Simplex QuorumCertificate and BLS aggregator #4091

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions simplex/qc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package simplex

import (
"errors"
"fmt"

"github.com/ava-labs/simplex"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
)

var (
_ simplex.QuorumCertificate = (*QC)(nil)
_ simplex.QCDeserializer = QCDeserializer{}
_ simplex.SignatureAggregator = (*SignatureAggregator)(nil)

// QC errors
errFailedToParseQC = errors.New("failed to parse quorum certificate")
errUnexpectedSigners = errors.New("unexpected number of signers in quorum certificate")
errSignatureAggregation = errors.New("signature aggregation failed")
errEncodingMessageToSign = errors.New("failed to encode message to sign")
)

// QC represents a quorum certificate in the Simplex consensus protocol.
type QC struct {
verifier *BLSVerifier
sig *bls.Signature
signers []simplex.NodeID
}

type SerializedQC struct {
Sig []byte `serialize:"true"`
Signers []simplex.NodeID `serialize:"true"`
}

// Signers returns the list of signers for the quorum certificate.
func (qc *QC) Signers() []simplex.NodeID {
return qc.signers
}

// Verify checks if the quorum certificate is valid by verifying the aggregated signature against the signers' public keys.
func (qc *QC) Verify(msg []byte) error {
pks := make([]*bls.PublicKey, 0, len(qc.signers))
quorum := simplex.Quorum(len(qc.verifier.nodeID2PK))
if len(qc.signers) != quorum {
return fmt.Errorf("%w: expected %d signers but got %d", errUnexpectedSigners, quorum, len(qc.signers))
}

// ensure all signers are in the membership set
for _, signer := range qc.signers {
pk, exists := qc.verifier.nodeID2PK[ids.NodeID(signer)]
if !exists {
return fmt.Errorf("%w: %x", errSignerNotFound, signer)
}

pks = append(pks, pk)
}

// aggregate the public keys
aggPK, err := bls.AggregatePublicKeys(pks)
if err != nil {
return fmt.Errorf("%w: %w", errSignatureAggregation, err)
}

message2Verify, err := encodeMessageToSign(msg, qc.verifier.chainID, qc.verifier.networkID)
if err != nil {
return fmt.Errorf("%w: %w", errEncodingMessageToSign, err)
}

if !bls.Verify(aggPK, qc.sig, message2Verify) {
return errSignatureVerificationFailed
}

return nil
}

// Bytes serializes the quorum certificate into bytes.
func (qc *QC) Bytes() []byte {
serializedQC := &SerializedQC{
Sig: bls.SignatureToBytes(qc.sig),
Signers: qc.signers,
}

bytes, err := Codec.Marshal(CodecVersion, serializedQC)
if err != nil {
panic(fmt.Errorf("failed to marshal QC: %w", err))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: don't panic, update the simplex interface to allow an error return value

}
return bytes
}

type QCDeserializer BLSVerifier

// DeserializeQuorumCertificate deserializes a quorum certificate from bytes.
func (d QCDeserializer) DeserializeQuorumCertificate(bytes []byte) (simplex.QuorumCertificate, error) {
var serializedQC SerializedQC
if _, err := Codec.Unmarshal(bytes, &serializedQC); err != nil {
return nil, fmt.Errorf("%w: %w", errFailedToParseQC, err)
}

sig, err := bls.SignatureFromBytes(serializedQC.Sig)
if err != nil {
return nil, fmt.Errorf("%w: %w", errFailedToParseSignature, err)
}

qc := QC{
sig: sig,
signers: serializedQC.Signers,
}

verifier := BLSVerifier(d)
qc.verifier = &verifier

return &qc, nil
}

// SignatureAggregator aggregates signatures into a quorum certificate.
type SignatureAggregator BLSVerifier

// Aggregate aggregates the provided signatures into a quorum certificate.
// It requires at least a quorum of signatures to succeed.
// If any signature is from a signer not in the membership set, it returns an error.
func (a SignatureAggregator) Aggregate(signatures []simplex.Signature) (simplex.QuorumCertificate, error) {
quorumSize := simplex.Quorum(len(a.nodeID2PK))
if len(signatures) < quorumSize {
return nil, fmt.Errorf("%w: expected %d signatures but got %d", errUnexpectedSigners, quorumSize, len(signatures))
}
signatures = signatures[:quorumSize]

signers := make([]simplex.NodeID, 0, quorumSize)
sigs := make([]*bls.Signature, 0, quorumSize)
for _, signature := range signatures {
signer := signature.Signer
_, exists := a.nodeID2PK[ids.NodeID(signer)]
if !exists {
return nil, fmt.Errorf("%w: %x", errSignerNotFound, signer)
}
signers = append(signers, signer)
sig, err := bls.SignatureFromBytes(signature.Value)
if err != nil {
return nil, fmt.Errorf("%w: %w", errFailedToParseSignature, err)
}
sigs = append(sigs, sig)
}

aggregatedSig, err := bls.AggregateSignatures(sigs)
if err != nil {
return nil, fmt.Errorf("%w: %w", errSignatureAggregation, err)
}

verifier := BLSVerifier(a)
return &QC{
verifier: &verifier,
signers: signers,
sig: aggregatedSig,
}, nil
}
220 changes: 220 additions & 0 deletions simplex/qc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package simplex

import (
"testing"

"github.com/ava-labs/simplex"
"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/crypto/bls"
)

// TestQCAggregateAndSign tests the aggregation of multiple signatures
// and then verifies the generated quorum certificate on that message.
func TestQCAggregateAndSign(t *testing.T) {
configs := newNetworkConfigs(t, 2)
quorum := simplex.Quorum(len(configs))

msg := []byte("Begin at the beginning, and go on till you come to the end: then stop")

signatures := make([]simplex.Signature, 0, quorum)
nodes := make([]simplex.NodeID, 0, quorum)
var signer BLSSigner
var verifier BLSVerifier

for _, config := range configs {
signer, verifier = NewBLSAuth(config)

sig, err := signer.Sign(msg)
require.NoError(t, err)
require.NoError(t, verifier.Verify(msg, sig, config.Ctx.NodeID[:]))

signatures = append(signatures, simplex.Signature{
Signer: config.Ctx.NodeID[:],
Value: sig,
})
nodes = append(nodes, config.Ctx.NodeID[:])
}

// aggregate the signatures into a quorum certificate
signatureAggregator := SignatureAggregator(verifier)
qc, err := signatureAggregator.Aggregate(signatures)

require.NoError(t, err)
require.Equal(t, nodes, qc.Signers())
// verify the quorum certificate
require.NoError(t, qc.Verify(msg))

d := QCDeserializer(verifier)
// try to deserialize the quorum certificate
deserializedQC, err := d.DeserializeQuorumCertificate(qc.Bytes())
require.NoError(t, err)

require.Equal(t, qc.Signers(), deserializedQC.Signers())
require.Equal(t, qc.Bytes(), deserializedQC.Bytes())
require.NoError(t, deserializedQC.Verify(msg))
}

func TestQCSignerNotInMembershipSet(t *testing.T) {
node1 := newEngineConfig(t, 2)
signer, verifier := NewBLSAuth(node1)

// nodes 1 and 2 will sign the same message
msg := []byte("Begin at the beginning, and go on till you come to the end: then stop")
sig, err := signer.Sign(msg)
require.NoError(t, err)
require.NoError(t, verifier.Verify(msg, sig, node1.Ctx.NodeID[:]))

// add a new validator, but it won't be in the membership set of the first node signer/verifier
node2 := newEngineConfig(t, 2)
node2.Ctx.ChainID = node1.Ctx.ChainID

// sign the same message with the new node
signer2, verifier2 := NewBLSAuth(node2)
sig2, err := signer2.Sign(msg)
require.NoError(t, err)
require.NoError(t, verifier2.Verify(msg, sig2, node2.Ctx.NodeID[:]))

// aggregate the signatures into a quorum certificate
signatureAggregator := SignatureAggregator(verifier)
_, err = signatureAggregator.Aggregate(
[]simplex.Signature{
{Signer: node1.Ctx.NodeID[:], Value: sig},
{Signer: node2.Ctx.NodeID[:], Value: sig2},
},
)
require.ErrorIs(t, err, errSignerNotFound)
}

func TestQCDeserializerInvalidInput(t *testing.T) {
config := newEngineConfig(t, 2)

_, verifier := NewBLSAuth(config)
deserializer := QCDeserializer(verifier)

tests := []struct {
name string
input []byte
err error
}{
{
name: "too short input",
input: make([]byte, 10),
err: errFailedToParseQC,
},
{
name: "invalid signature bytes",
input: make([]byte, simplex.Quorum(len(verifier.nodeID2PK))*ids.NodeIDLen+bls.SignatureLen),
err: errFailedToParseQC,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := deserializer.DeserializeQuorumCertificate(tt.input)
require.ErrorIs(t, err, tt.err)
})
}
}

func TestSignatureAggregatorInsufficientSignatures(t *testing.T) {
config := newEngineConfig(t, 3)

signer, verifier := NewBLSAuth(config)
msg := []byte("test message")
sig, err := signer.Sign(msg)
require.NoError(t, err)

// try to aggregate with only 1 signature when quorum is 2
signatureAggregator := SignatureAggregator(verifier)
_, err = signatureAggregator.Aggregate(
[]simplex.Signature{
{Signer: config.Ctx.NodeID[:], Value: sig},
},
)
require.ErrorIs(t, err, errUnexpectedSigners)
}

func TestSignatureAggregatorInvalidSignatureBytes(t *testing.T) {
config := newEngineConfig(t, 2)

signer, verifier := NewBLSAuth(config)
msg := []byte("test message")
sig, err := signer.Sign(msg)
require.NoError(t, err)

signatureAggregator := SignatureAggregator(verifier)
_, err = signatureAggregator.Aggregate(
[]simplex.Signature{
{Signer: config.Ctx.NodeID[:], Value: sig},
{Signer: config.Ctx.NodeID[:], Value: []byte("invalid signature")},
},
)
require.ErrorIs(t, err, errFailedToParseSignature)
}

func TestSignatureAggregatorExcessSignatures(t *testing.T) {
configs := newNetworkConfigs(t, 4)

msg := []byte("test message")

var nodeSigner BLSSigner
var verifier BLSVerifier

// Create signatures from all 4 nodes
signatures := make([]simplex.Signature, 4)
for i, config := range configs {
nodeSigner, verifier = NewBLSAuth(config)
sig, err := nodeSigner.Sign(msg)
require.NoError(t, err)

signatures[i] = simplex.Signature{Signer: config.Ctx.NodeID[:], Value: sig}
}

// Aggregate should only use the first 3 signatures
signatureAggregator := SignatureAggregator(verifier)
qc, err := signatureAggregator.Aggregate(signatures)
require.NoError(t, err)

// Should only have 3 signers, not 4
require.Len(t, qc.Signers(), simplex.Quorum(len(configs)))
require.NoError(t, qc.Verify(msg))
}

func TestQCVerifyWithWrongMessage(t *testing.T) {
configs := newNetworkConfigs(t, 2)

node1 := configs[0]
node2 := configs[1]
signer, verifier := NewBLSAuth(node1)
originalMsg := []byte("original message")
wrongMsg := []byte("wrong message")

// Create signatures for original message
sig1, err := signer.Sign(originalMsg)
require.NoError(t, err)

signer2, _ := NewBLSAuth(node2)
sig2, err := signer2.Sign(originalMsg)
require.NoError(t, err)

signatureAggregator := SignatureAggregator(verifier)
qc, err := signatureAggregator.Aggregate(
[]simplex.Signature{
{Signer: node1.Ctx.NodeID[:], Value: sig1},
{Signer: node2.Ctx.NodeID[:], Value: sig2},
},
)
require.NoError(t, err)

// Verify with original message should succeed
require.NoError(t, qc.Verify(originalMsg))

// Verify with wrong message should fail
err = qc.Verify(wrongMsg)
require.ErrorIs(t, err, errSignatureVerificationFailed)
}