diff --git a/go.mod b/go.mod index 78d0feccd1a1..18c41928d458 100644 --- a/go.mod +++ b/go.mod @@ -84,6 +84,7 @@ require ( github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/VictoriaMetrics/fastcache v1.12.1 // indirect + github.com/ava-labs/simplex v0.0.0-20250611154800-78b82e9820e5 github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect diff --git a/go.sum b/go.sum index e2231bc73fbc..5eedbfcb4301 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,8 @@ github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60 h1:EL github.com/ava-labs/ledger-avalanche/go v0.0.0-20241009183145-e6f90a8a1a60/go.mod h1:/7qKobTfbzBu7eSTVaXMTr56yTYk4j2Px6/8G+idxHo= github.com/ava-labs/libevm v0.0.0-20250610142802-2672fbd7cdfc h1:cSXaUY4hdmoJ2FJOgOzn+WiovN/ZB/zkNRgnZhE50OA= github.com/ava-labs/libevm v0.0.0-20250610142802-2672fbd7cdfc/go.mod h1:+Iol+sVQ1KyoBsHf3veyrBmHCXr3xXRWq6ZXkgVfNLU= +github.com/ava-labs/simplex v0.0.0-20250611154800-78b82e9820e5 h1:rwPm63i5nJ2XIuNjO2H68gDmMKje0VW7orLZMISPrC8= +github.com/ava-labs/simplex v0.0.0-20250611154800-78b82e9820e5/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= diff --git a/simplex/bls.go b/simplex/bls.go new file mode 100644 index 000000000000..7530ea55bf4e --- /dev/null +++ b/simplex/bls.go @@ -0,0 +1,124 @@ +// 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 ( + errSignatureVerificationFailed = errors.New("signature verification failed") + 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") +) + +var _ simplex.Signer = (*BLSSigner)(nil) + +type SignFunc func(msg []byte) (*bls.Signature, error) + +// BLSSigner signs messages encoded with the provided ChainID and NetworkID. +// using the SignBLS function. +type BLSSigner struct { + chainID ids.ID + networkID uint32 + // signBLS is passed in because we support both software and hardware BLS signing. + signBLS SignFunc +} + +type BLSVerifier struct { + nodeID2PK map[ids.NodeID]*bls.PublicKey + networkID uint32 + chainID ids.ID +} + +func NewBLSAuth(config *Config) (BLSSigner, BLSVerifier) { + verifier := createVerifier(config) + + return BLSSigner{ + chainID: config.Ctx.ChainID, + networkID: config.Ctx.NetworkID, + signBLS: config.SignBLS, + }, verifier +} + +// Sign returns a signature on the given message using BLS signature scheme. +// It encodes the message to sign with the chain ID, and network ID, +func (s *BLSSigner) Sign(message []byte) ([]byte, error) { + message2Sign, err := encodeMessageToSign(message, s.chainID, s.networkID) + if err != nil { + return nil, fmt.Errorf("failed to encode message to sign: %w", err) + } + + sig, err := s.signBLS(message2Sign) + if err != nil { + return nil, err + } + + sigBytes := bls.SignatureToBytes(sig) + return sigBytes, nil +} + +type encodedSimplexSignedPayload struct { + NewtorkID uint32 `serialize:"true"` + ChainID ids.ID `serialize:"true"` + Message []byte `serialize:"true"` +} + +func encodeMessageToSign(message []byte, chainID ids.ID, networkID uint32) ([]byte, error) { + encodedSimplexMessage := encodedSimplexSignedPayload{ + Message: message, + ChainID: chainID, + NewtorkID: networkID, + } + return Codec.Marshal(CodecVersion, &encodedSimplexMessage) +} + +func (v BLSVerifier) Verify(message []byte, signature []byte, signer simplex.NodeID) error { + key, err := ids.ToNodeID(signer) + if err != nil { + return fmt.Errorf("%w: %w", errInvalidNodeID, err) + } + + pk, exists := v.nodeID2PK[key] + if !exists { + return fmt.Errorf("%w: signer %x", errSignerNotFound, key) + } + + sig, err := bls.SignatureFromBytes(signature) + if err != nil { + return fmt.Errorf("%w: %w", errFailedToParseSignature, err) + } + + message2Verify, err := encodeMessageToSign(message, v.chainID, v.networkID) + if err != nil { + return fmt.Errorf("failed to encode message to verify: %w", err) + } + + if !bls.Verify(pk, sig, message2Verify) { + return errSignatureVerificationFailed + } + + return nil +} + +func createVerifier(config *Config) BLSVerifier { + verifier := BLSVerifier{ + nodeID2PK: make(map[ids.NodeID]*bls.PublicKey), + networkID: config.Ctx.NetworkID, + chainID: config.Ctx.ChainID, + } + + for _, node := range config.Validators { + verifier.nodeID2PK[node.NodeID] = node.PublicKey + } + + return verifier +} diff --git a/simplex/bls_test.go b/simplex/bls_test.go new file mode 100644 index 000000000000..cc38dd6d918d --- /dev/null +++ b/simplex/bls_test.go @@ -0,0 +1,88 @@ +// 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/utils/crypto/bls" +) + +func TestBLSVerifier(t *testing.T) { + config, err := newEngineConfig() + require.NoError(t, err) + signer, verifier := NewBLSAuth(config) + otherNodeID := ids.GenerateTestNodeID() + + msg := []byte("Begin at the beginning, and go on till you come to the end: then stop") + tests := []struct { + name string + expectErr error + nodeID []byte + sig []byte + }{ + { + name: "valid_signature", + expectErr: nil, + nodeID: config.Ctx.NodeID[:], + sig: func() []byte { + sig, err := signer.Sign(msg) + require.NoError(t, err) + return sig + }(), + }, + { + name: "not_in_membership_set", + expectErr: errSignerNotFound, + nodeID: otherNodeID[:], + sig: func() []byte { + sig, err := signer.Sign(msg) + require.NoError(t, err) + return sig + }(), + }, + { + name: "invalid_message_encoding", + expectErr: errSignatureVerificationFailed, + nodeID: config.Ctx.NodeID[:], + sig: func() []byte { + sig, err := config.SignBLS(msg) + require.NoError(t, err) + return bls.SignatureToBytes(sig) + }(), + }, + { + name: "invalid_nodeID", + expectErr: errInvalidNodeID, + nodeID: []byte{0x01, 0x02, 0x03, 0x04, 0x05}, // Incorrect length NodeID + sig: func() []byte { + sig, err := signer.Sign(msg) + require.NoError(t, err) + return sig + }(), + }, + { + name: "nil_signature", + expectErr: errFailedToParseSignature, + nodeID: config.Ctx.NodeID[:], + sig: nil, + }, + { + name: "malformed_signature", + expectErr: errFailedToParseSignature, + nodeID: config.Ctx.NodeID[:], + sig: []byte{0x01, 0x02, 0x03}, // Malformed signature + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err = verifier.Verify(msg, tt.sig, tt.nodeID) + require.ErrorIs(t, err, tt.expectErr) + }) + } +} diff --git a/simplex/codec.go b/simplex/codec.go new file mode 100644 index 000000000000..a79200ee4474 --- /dev/null +++ b/simplex/codec.go @@ -0,0 +1,26 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package simplex + +import ( + "math" + + "github.com/ava-labs/avalanchego/codec" + "github.com/ava-labs/avalanchego/codec/linearcodec" + "github.com/ava-labs/avalanchego/vms/platformvm/warp" +) + +const CodecVersion = warp.CodecVersion + 1 + +var Codec codec.Manager + +func init() { + lc := linearcodec.NewDefault() + + Codec = codec.NewManager(math.MaxInt) + + if err := Codec.RegisterCodec(CodecVersion, lc); err != nil { + panic(err) + } +} diff --git a/simplex/config.go b/simplex/config.go new file mode 100644 index 000000000000..e95f080e2249 --- /dev/null +++ b/simplex/config.go @@ -0,0 +1,36 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package simplex + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/logging" +) + +// Config wraps all the parameters needed for a simplex engine +type Config struct { + Ctx SimplexChainContext + Log logging.Logger + + // Validators is a map of node IDs to their validator information. + // This tells the node about the current membership set, and should be consistent + // across all nodes in the subnet. + Validators map[ids.NodeID]*validators.GetValidatorOutput + + // SignBLS is the signing function used for this node to sign messages. + SignBLS SignFunc +} + +// Context is information about the current execution. +type SimplexChainContext struct { + // Network is the ID of the network this context exists within. + NodeID ids.NodeID + + // ChainID is the ID of the chain this context exists within. + ChainID ids.ID + + // NodeID is the ID of this node + NetworkID uint32 +} diff --git a/simplex/test_util.go b/simplex/test_util.go new file mode 100644 index 000000000000..5b167bd17e8d --- /dev/null +++ b/simplex/test_util.go @@ -0,0 +1,46 @@ +// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package simplex + +import ( + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/snow/validators" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" +) + +func newTestValidatorInfo(allVds []validators.GetValidatorOutput) map[ids.NodeID]*validators.GetValidatorOutput { + vds := make(map[ids.NodeID]*validators.GetValidatorOutput, len(allVds)) + for _, vd := range allVds { + vds[vd.NodeID] = &vd + } + + return vds +} + +func newEngineConfig() (*Config, error) { + ls, err := localsigner.New() + if err != nil { + return nil, err + } + + nodeID := ids.GenerateTestNodeID() + + simplexChainContext := SimplexChainContext{ + NodeID: nodeID, + ChainID: ids.GenerateTestID(), + NetworkID: constants.UnitTestID, + } + + nodeInfo := validators.GetValidatorOutput{ + NodeID: nodeID, + PublicKey: ls.PublicKey(), + } + + return &Config{ + Ctx: simplexChainContext, + Validators: newTestValidatorInfo([]validators.GetValidatorOutput{nodeInfo}), + SignBLS: ls.Sign, + }, nil +}