Documentation ¶
Overview ¶
Package gblsminsig wraps github.com/supranational/blst/bindings/go to provide a gcrypto.PubKey implementation backed by BLS keys, where the BLS keys have minimized signatures.
We are not currently providing an alternate implementation with minimized keys, as signatures are expected to be transmitted and stored much more frequently than keys.
The blst dependency requires CGo, so therefore this package also requires CGo.
Two key references for correctly understanding and using BLS keys are RFC9380 (Hashing to Elliptic Curves) and the IETF draft for BLS Signatures.
See the SignatureProofScheme docs for a detailed explanation of how this package aggregates keys and signatures.
Index ¶
- Variables
- func NewPubKey(b []byte) (gcrypto.PubKey, error)
- func Register(reg *gcrypto.Registry)
- type PubKey
- type SignatureProof
- func (p SignatureProof) AddSignature(sig []byte, key gcrypto.PubKey) error
- func (p SignatureProof) AsSparse() gcrypto.SparseSignatureProof
- func (p SignatureProof) Clone() gcrypto.CommonMessageSignatureProof
- func (p SignatureProof) Derive() gcrypto.CommonMessageSignatureProof
- func (p SignatureProof) HasSparseKeyID(keyID []byte) (has, valid bool)
- func (p SignatureProof) Matches(other gcrypto.CommonMessageSignatureProof) bool
- func (p SignatureProof) Merge(other gcrypto.CommonMessageSignatureProof) gcrypto.SignatureProofMergeResult
- func (p SignatureProof) MergeSparse(s gcrypto.SparseSignatureProof) gcrypto.SignatureProofMergeResult
- func (p SignatureProof) Message() []byte
- func (p SignatureProof) PubKeyHash() []byte
- func (p SignatureProof) SignatureBitSet(dst *bitset.BitSet)
- type SignatureProofScheme
- func (SignatureProofScheme) CanMergeFinalizedProofs() bool
- func (SignatureProofScheme) Finalize(main gcrypto.CommonMessageSignatureProof, ...) gcrypto.FinalizedCommonMessageSignatureProof
- func (SignatureProofScheme) KeyIDChecker(keys []gcrypto.PubKey) gcrypto.KeyIDChecker
- func (SignatureProofScheme) New(msg []byte, keys []gcrypto.PubKey, pubKeyHash string) (gcrypto.CommonMessageSignatureProof, error)
- func (SignatureProofScheme) ValidateFinalizedProof(proof gcrypto.FinalizedCommonMessageSignatureProof, ...) (signBitsByHash map[string]*bitset.BitSet, allSignaturesUnique bool)
- type Signer
Constants ¶
This section is empty.
Variables ¶
var DomainSeparationTag = []byte("BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_")
The domain separation tag is a requirement per RFC9380 (Hashing to Elliptic Curves). See sections 2.2.5 (domain separation), 3.1 (domain separation requirements), and 8.10 (suite ID naming conventions).
Furthermore, see also draft-irtf-cfrg-bls-signature-05, section 4.1 (ciphersuite format), as that is the actual format being followed here.
The ciphersuite ID according to the BLS signature document is:
"BLS_SIG_" || H2C_SUITE_ID || SC_TAG || "_"
And the H2C_SUITE_ID, per RFC9380 section 8.8.1, is:
BLS12381G1_XMD:SHA-256_SSWU_RO_
Which only leaves the SC_TAG value, which is "NUL" for the basic scheme.
Functions ¶
Types ¶
type PubKey ¶
PubKey wraps a blst.P2Affine and defines methods for the gcrypto.PubKey interface.
func (PubKey) PubKeyBytes ¶
PubKeyBytes returns the compressed bytes underlying k's P2 affine point.
type SignatureProof ¶
type SignatureProof struct {
// contains filtered or unexported fields
}
SignatureProof is an implementation of gcrypto.CommonMessageSignatureProof for the BLS keys and signatures in this package.
When extracting sparse signatures from this proof, signatures are aggregated pairwise, forming a binary tree. If signatures were free to be paired arbitrarily, then a validator could receive an aggregation of A-B and then a separate aggregation of B-C-D. Aggregating them into A-B-B-C-D is valid in general, but then you need to either have a way to indicate that B has been accounted for twice, or you need a way to recover the original signature B in order to subtract B to normalize it back to A-B-C-D.
Instead, all validators with the same view of the public keys understand how to aggregate keys and signatures in a fixed fashion. Arranging the validators such that the leftmost validators are the most likely to be online and voting the same way, allows the signatures to be more likely aggregated into a single set, thereby minimizing bandwidth during consensus gossip.
func NewSignatureProof ¶
func NewSignatureProof(msg []byte, trustedKeys []PubKey, pubKeyHash string) (SignatureProof, error)
NewSignatureProof returns a new SignatureProof based on trustedKeys.
The pubKeyHash is sent as part of the sparse signatures, and it is meant to ensure that peers agree on the set of keys and corresponding signatures.
It may turn out that we need a pair of key hashes -- one for the real set of ordered validator keys, and another hash representing the current arrangement of keys for the proof. For instance, if a highly delegated validator has not voted in the past several blocks, that validator ought to move towards the end of the list such that its absence does not interfere with aggregating the other online validators' signatures.
func (SignatureProof) AddSignature ¶
func (p SignatureProof) AddSignature(sig []byte, key gcrypto.PubKey) error
AddSignature adds a signature representing a single key.
This should only be called when receiving the local application's signature for a message. Otherwise, use the Merge method to combine incoming proofs with the existing one.
If the signature does not match, or if the public key was not one of the candidate keys, an error is returned.
func (SignatureProof) AsSparse ¶
func (p SignatureProof) AsSparse() gcrypto.SparseSignatureProof
func (SignatureProof) Clone ¶
func (p SignatureProof) Clone() gcrypto.CommonMessageSignatureProof
func (SignatureProof) Derive ¶
func (p SignatureProof) Derive() gcrypto.CommonMessageSignatureProof
func (SignatureProof) HasSparseKeyID ¶
func (p SignatureProof) HasSparseKeyID(keyID []byte) (has, valid bool)
HasSparseKeyID reports whether the full proof already contains a signature matching the given sparse key ID. If the key ID does not properly map into the set of trusted public keys, the "valid" return parameter will be false.
func (SignatureProof) Matches ¶
func (p SignatureProof) Matches(other gcrypto.CommonMessageSignatureProof) bool
func (SignatureProof) Merge ¶
func (p SignatureProof) Merge(other gcrypto.CommonMessageSignatureProof) gcrypto.SignatureProofMergeResult
func (SignatureProof) MergeSparse ¶
func (p SignatureProof) MergeSparse(s gcrypto.SparseSignatureProof) gcrypto.SignatureProofMergeResult
func (SignatureProof) Message ¶
func (p SignatureProof) Message() []byte
func (SignatureProof) PubKeyHash ¶
func (p SignatureProof) PubKeyHash() []byte
func (SignatureProof) SignatureBitSet ¶
func (p SignatureProof) SignatureBitSet(dst *bitset.BitSet)
type SignatureProofScheme ¶
type SignatureProofScheme struct{}
SignatureProofScheme is a gcrypto.SignatureProofScheme for the minimal-sized-signature BLS format.
At a high level, the most important detail to understand about BLS is that any number keys and signatures can be respectively aggregated into a single key or signature. Performing the aggregation is relatively cheap, and verifying an aggregated signature is about as expensive as verifying an unaggregated one.
Without getting deep into the math behind BLS, keys and signatures exist as points on elliptic curves, and points on those curves have the property that they can be added and subtracted. Those additions and subtractions are commutative, just like everyday addition and subtraction that we are used to. That means if we have keys A, B, C, and D, it doesn't matter if we do (A + (B + (C + D))) or (D + (A + (C + B))); the result is the same regardless of order.
However, to correctly distribute the aggregated keys and signatures during validator gossip, we do have to be particular about how we aggregate. First consider a naive case with the A, B, C, and D key example above. We will consider "A" to mean the combination of both the key and the signature belonging to A. Suppose you get one message of the aggregation (A+B) and another message of the aggregation (B+D). You are allowed to aggregate those two messages, resulting in (A+B+B+D), but this unorganized pattern is unsustainable at scale. If the next message you got was (B+C+D), you could aggregate that into (A+B+B+B+C+D+D). In fact you could verify that signature properly if you continue to track which keys and signatures were accumulated more than once, but if we require the network to track counts of how many times a pairing is present, it ends up quickly becoming a secondary and irrelevant detail to track.
So instead of allowing arbitrary aggregation during gossip, we require the aggregations to be pairwise in a tree format. If our keyset was the 8 keys, A, B, C, D, E, F, G, and H, then valid pairings include A+B, C+D, A+B+C+D, or A+B+C+D+E+F+G+H. Invalid pairings include B+C (because that would leave A, the first key, unpaired), or A+B+E+F (while A+B and E+F are independently correct, they are not a contiguous collection).
The pairwise tree format also allows us to use a single number to represent a determinstic pairing, which is much more efficient than using bitsets. For example, while numbers 0-7 indicate the independent keys A-H, then index 8 would indicate A+B, 9 would indicate C+D, all the way up to 15 indicating the full aggregation of A through H. (A binary tree with n leaves when n is a power of two then has (2*n)-1 total nodes.) Transmitting that single number is currently encoded as a 16-bit integer key ID in the sparse signature, which is significantly less data to transmit than the aggregated key, while remaining completely sufficient for any other validator to decode.
When we get to signature finalization -- that is, writing a previous commit proof in a block header -- we no longer abide by the pairwise rules, because the finalized signature is not intended to be modified further.
There are two parts to finalized signatures. First is the "main" signature: this is signing the block that is being committed. The main signature is always present. Besides the main signature, there is a collection of zero or more signatures for any other votes, which could be votes for nil or votes for a different block.
The key IDs for the finalized signatures are represented in two parts. First is a two-byte header, a big-endian uint16, representing the number of signatures aggregated to produce the finalized signature. The remaining bytes are a big-endian encoding of the combinatorial index of the present signatures from the key space. The meaning of "key space" varies slightly depending on what part of the finalized signature we are inspecting. For the main signature, the key space is the full original key set, so if there were 100 validators and we indicate 90 of them voted for the block, then that index indicates exactly which 90 keys make up that aggregation.
Then the non-main signatures follow a specific ordering. Suppose of the remaining 10, there were 6 votes for nil, and then 2 votes for some other block. We order by the number of votes descending, and in case of a tie then it goes by sign content ascending.
The combinatorial index for the 6 votes for nil are encoded as "10 choose 6" because we know that 90 of the 100 signatures were already used; then the two votes for the other block are encoded as "4 choose 2" The last two absent votes have no representation in the finalized signature. Using a reduced key space here allows us to pack signature representations into fewer bytes, as for example "4 choose 2" only has 6 possible values and "100 choose 2" has 4,950 possible values.
func (SignatureProofScheme) CanMergeFinalizedProofs ¶
func (SignatureProofScheme) CanMergeFinalizedProofs() bool
func (SignatureProofScheme) Finalize ¶
func (SignatureProofScheme) Finalize( main gcrypto.CommonMessageSignatureProof, rest []gcrypto.CommonMessageSignatureProof, ) gcrypto.FinalizedCommonMessageSignatureProof
func (SignatureProofScheme) KeyIDChecker ¶
func (SignatureProofScheme) KeyIDChecker(keys []gcrypto.PubKey) gcrypto.KeyIDChecker
func (SignatureProofScheme) New ¶
func (SignatureProofScheme) New(msg []byte, keys []gcrypto.PubKey, pubKeyHash string) ( gcrypto.CommonMessageSignatureProof, error, )
func (SignatureProofScheme) ValidateFinalizedProof ¶
func (SignatureProofScheme) ValidateFinalizedProof( proof gcrypto.FinalizedCommonMessageSignatureProof, hashesBySignContent map[string]string, ) ( signBitsByHash map[string]*bitset.BitSet, allSignaturesUnique bool, )
type Signer ¶
type Signer struct {
// contains filtered or unexported fields
}
Signer satisfies gcrypto.Signer for minimized-signature BLS.
func NewSigner ¶
NewSigner returns a new signer. The initial key material must be at least 32 bytes, and should be cryptographically random.