diff --git a/simplex/bls.go b/simplex/bls.go index 7530ea55bf4e..9ad501175ebd 100644 --- a/simplex/bls.go +++ b/simplex/bls.go @@ -1,6 +1,3 @@ -// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. -// See the file LICENSE for licensing terms. - package simplex import ( @@ -18,6 +15,13 @@ var ( errSignerNotFound = errors.New("signer not found in the membership set") errInvalidNodeID = errors.New("unable to parse node ID") errFailedToParseSignature = errors.New("failed to parse signature") + + // QC errors + errFailedToParseQC = errors.New("failed to parse quorum certificate") + errNotEnoughSigners = errors.New("not enough signers") + errSignatureAggregation = errors.New("signature aggregation failed") + errEncodingMessageToSign = errors.New("failed to encode message to sign") + simplexLabel = []byte("simplex") ) var _ simplex.Signer = (*BLSSigner)(nil) diff --git a/simplex/bls_test.go b/simplex/bls_test.go index cc38dd6d918d..37ace583738e 100644 --- a/simplex/bls_test.go +++ b/simplex/bls_test.go @@ -10,6 +10,7 @@ import ( "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/crypto/bls" + "github.com/ava-labs/simplex" ) func TestBLSVerifier(t *testing.T) { @@ -86,3 +87,237 @@ func TestBLSVerifier(t *testing.T) { }) } } + + +func TestSignerNotInMemberSet(t *testing.T) { + config := newEngineConfig(t, 1) + signer, verifier := NewBLSAuth(&config.Config) + + msg := "Begin at the beginning, and go on till you come to the end: then stop" + + sig, err := signer.Sign([]byte(msg)) + require.NoError(t, err) + + notInMembershipSet := ids.GenerateTestNodeID() + err = verifier.Verify([]byte(msg), sig, notInMembershipSet[:]) + require.ErrorIs(t, err, errSignerNotFound) +} + +func TestSignerInvalidMessageEncoding(t *testing.T) { + config := newEngineConfig(t, 1) + + // sign a message with invalid encoding + dummyMsg := []byte("dummy message") + sig, err := config.SignBLS(dummyMsg) + require.NoError(t, err) + + sigBytes := bls.SignatureToBytes(sig) + + _, verifier := NewBLSAuth(&config.Config) + err = verifier.Verify(dummyMsg, sigBytes, config.Ctx.NodeID[:]) + require.ErrorIs(t, err, errSignatureVerificationFailed) +} + +// TestQCAggregateAndSign tests the aggregation of multiple signatures +// and then verifies the generated quorum certificate on that message. +func TestQCAggregateAndSign(t *testing.T) { + config := newEngineConfig(t, 2) + nodeID1 := config.Ctx.NodeID + + signer, verifier := NewBLSAuth(&config.Config) + // 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, config.Ctx.NodeID[:])) + + node2 := config.Nodes[1] + config.SignBLS = node2.sign + signer2, verifier2 := NewBLSAuth(&config.Config) + sig2, err := signer2.Sign(msg) + require.NoError(t, err) + require.NoError(t, verifier2.Verify(msg, sig2, node2.NodeID[:])) + + // aggregate the signatures into a quorum certificate + signatureAggregator := SignatureAggregator(verifier) + qc, err := signatureAggregator.Aggregate( + []simplex.Signature{ + {Signer: nodeID1[:], Value: sig}, + {Signer: node2.NodeID[:], Value: sig2}, + }, + ) + require.NoError(t, err) + require.Equal(t, []simplex.NodeID{config.Ctx.NodeID[:], node2.NodeID[:]}, 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.NoError(t, deserializedQC.Verify(msg)) + require.Equal(t, qc.Bytes(), deserializedQC.Bytes()) +} + +func TestQCSignerNotInMembershipSet(t *testing.T) { + config := newEngineConfig(t, 2) + nodeID1 := config.Ctx.NodeID + + signer, verifier := NewBLSAuth(&config.Config) + // 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, config.Ctx.NodeID[:])) + + // add a new validator, but it won't be in the membership set of the first node signer/verifier + vds := generateTestValidators(t, 1) + config.Ctx.NodeID = vds[0].NodeID + config.Validators[vds[0].NodeID] = &vds[0].GetValidatorOutput + config.SignBLS = vds[0].sign + + // sign the same message with the new node + signer2, verifier2 := NewBLSAuth(&config.Config) + sig2, err := signer2.Sign(msg) + require.NoError(t, err) + require.NoError(t, verifier2.Verify(msg, sig2, vds[0].NodeID[:])) + + // aggregate the signatures into a quorum certificate + signatureAggregator := SignatureAggregator(verifier) + _, err = signatureAggregator.Aggregate( + []simplex.Signature{ + {Signer: nodeID1[:], Value: sig}, + {Signer: config.Ctx.NodeID[:], Value: sig2}, + }, + ) + require.ErrorIs(t, err, errSignerNotFound) +} + +func TestQCDeserializerInvalidInput(t *testing.T) { + config := newEngineConfig(t, 2) + + _, verifier := NewBLSAuth(&config.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.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, errNotEnoughSigners) +} + +func TestSignatureAggregatorInvalidSignatureBytes(t *testing.T) { + config := newEngineConfig(t, 2) + + signer, verifier := NewBLSAuth(&config.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) { + config := newEngineConfig(t, 4) + + _, verifier := NewBLSAuth(&config.Config) + msg := []byte("test message") + + // Create signatures from all 4 nodes + signatures := make([]simplex.Signature, 4) + for i, node := range config.Nodes { + config.SignBLS = node.sign + nodeSigner, _ := NewBLSAuth(&config.Config) + sig, err := nodeSigner.Sign(msg) + require.NoError(t, err) + + signatures[i] = simplex.Signature{Signer: node.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(config.Nodes))) + require.NoError(t, qc.Verify(msg)) +} + +func TestQCVerifyWithWrongMessage(t *testing.T) { + config := newEngineConfig(t, 2) + + signer, verifier := NewBLSAuth(&config.Config) + originalMsg := []byte("original message") + wrongMsg := []byte("wrong message") + + // Create signatures for original message + sig1, err := signer.Sign(originalMsg) + require.NoError(t, err) + + config.SignBLS = config.Nodes[1].sign + signer2, _ := NewBLSAuth(&config.Config) + sig2, err := signer2.Sign(originalMsg) + require.NoError(t, err) + + signatureAggregator := SignatureAggregator(verifier) + qc, err := signatureAggregator.Aggregate( + []simplex.Signature{ + {Signer: config.Nodes[0].NodeID[:], Value: sig1}, + {Signer: config.Nodes[1].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) +} diff --git a/simplex/quorum.go b/simplex/quorum.go new file mode 100644 index 000000000000..300b8e355a10 --- /dev/null +++ b/simplex/quorum.go @@ -0,0 +1,177 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package simplex + +import ( + "encoding/asn1" + "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 represents a quorum certificate in the Simplex consensus protocol. +type QC struct { + verifier BLSVerifier + sig bls.Signature + signers []simplex.NodeID +} + +// 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)) + if len(qc.signers) != simplex.Quorum(len(qc.verifier.nodeID2PK)) { + return fmt.Errorf("%w: expected %d signers but got %d", errNotEnoughSigners, simplex.Quorum(len(qc.verifier.nodeID2PK)), 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 +} + +// asn1QC is the ASN.1 structure for the quorum certificate. +// It contains the signers' public keys and the aggregated signature. +// The signers are represented as byte slices of their IDs. +type asn1QC struct { + Signers [][]byte + Signature []byte +} + +func (qc *QC) MarshalASN1() ([]byte, error) { + sigBytes := bls.SignatureToBytes(&qc.sig) + + signersBytes := make([][]byte, len(qc.signers)) + for i, signer := range qc.signers { + s := signer // avoid aliasing + signersBytes[i] = s[:] + } + asn1Data := asn1QC{ + Signers: signersBytes, + Signature: sigBytes, + } + return asn1.Marshal(asn1Data) +} + +func (qc *QC) UnmarshalASN1(data []byte) error { + var decoded asn1QC + _, err := asn1.Unmarshal(data, &decoded) + if err != nil { + return err + } + qc.signers = make([]simplex.NodeID, len(decoded.Signers)) + for i, signerBytes := range decoded.Signers { + if len(signerBytes) != ids.ShortIDLen { // TODO: so long as simplex is in a separate repo, we should decouple these ids as much as possible + return errors.New("invalid signer length") + } + qc.signers[i] = simplex.NodeID(signerBytes) + } + sig, err := bls.SignatureFromBytes(decoded.Signature) + if err != nil { + return err + } + qc.sig = *sig + + return nil +} + +// Bytes serializes the quorum certificate into bytes. +func (qc *QC) Bytes() []byte { + bytes, err := qc.MarshalASN1() + if err != nil { + panic(fmt.Errorf("failed to marshal QC: %w", err)) + } + return bytes +} + +type QCDeserializer BLSVerifier + +// DeserializeQuorumCertificate deserializes a quorum certificate from bytes. +func (d QCDeserializer) DeserializeQuorumCertificate(bytes []byte) (simplex.QuorumCertificate, error) { + var qc QC + if err := qc.UnmarshalASN1(bytes); err != nil { + return nil, fmt.Errorf("%w: %w", errFailedToParseQC, err) + } + qc.verifier = BLSVerifier(d) + + 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: wanted %d signatures but got %d", errNotEnoughSigners, 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) + } + + return &QC{ + verifier: BLSVerifier(a), + signers: signers, + sig: *aggregatedSig, + }, nil +} diff --git a/simplex/util_test.go b/simplex/util_test.go new file mode 100644 index 000000000000..7221194bf746 --- /dev/null +++ b/simplex/util_test.go @@ -0,0 +1,92 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package simplex + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" +) + +type testConfig struct { + Config + Nodes []*testSigner +} + +type testSigner struct { + validators.GetValidatorOutput + sign SignFunc +} + +func newTestValidators(allVds []*testSigner) map[ids.NodeID]*validators.GetValidatorOutput { + vds := make(map[ids.NodeID]*validators.GetValidatorOutput, len(allVds)) + for _, vd := range allVds { + vds[vd.NodeID] = &validators.GetValidatorOutput{ + NodeID: vd.NodeID, + PublicKey: vd.PublicKey, + Weight: 1, // Default weight for testing + } + } + + return vds +} + +// func newEngineConfig(t *testing.T, numNodes uint64) *testConfig { +// if numNodes == 0 { +// panic("numNodes must be greater than 0") +// } + +// ls, err := localsigner.New() +// require.NoError(t, err) + +// nodeID := ids.GenerateTestNodeID() + +// simplexChainContext := SimplexChainContext{ +// NodeID: nodeID, +// ChainID: ids.GenerateTestID(), +// SubnetID: ids.GenerateTestID(), +// } + +// nodeInfo := &testSigner{ +// GetValidatorOutput: validators.GetValidatorOutput{ +// NodeID: nodeID, +// PublicKey: ls.PublicKey(), +// }, +// sign: ls.Sign, +// } + +// nodes := generateTestValidators(t, numNodes-1) +// nodes = append([]*testSigner{nodeInfo}, nodes...) + +// return &testConfig{ +// Config: Config{ +// Ctx: simplexChainContext, +// Validators: newTestValidators(nodes), +// SignBLS: ls.Sign, +// }, +// Nodes: nodes, +// } +// } + +func generateTestValidators(t *testing.T, num uint64) []*testSigner { + vds := make([]*testSigner, num) + for i := uint64(0); i < num; i++ { + ls, err := localsigner.New() + require.NoError(t, err) + + nodeID := ids.GenerateTestNodeID() + vds[i] = &testSigner{ + GetValidatorOutput: validators.GetValidatorOutput{ + NodeID: nodeID, + PublicKey: ls.PublicKey(), + }, + sign: ls.Sign, + } + } + return vds +}