LIP: 0038
Title: Introduce BLS signatures
Author: Andreas Kendziorra <andreas.kendziorra@lightcurve.io>
Discussions-To: https://research.lisk.com/t/introduce-bls-signatures/282
Status: Active
Type: Informational
Created: 2021-04-13
Updated: 2024-01-04
This document specifies how to use BLS signatures within Lisk. In particular, it specifies how to create and validate compact aggregate signatures with BLS. The specification consists mainly of a choice of a ciphersuite, i.e., the choice of a concrete BLS variant including the choice of several parameters. Moreover, some guidelines on how to use it within a blockchain created with the Lisk SDK are given.
This document does not specify any concrete applications of BLS signatures nor does it impose any protocol changes. It is purely informational on how to use them if desired. Specific applications need to be defined in separate LIPs.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
The purpose of this LIP is to be prepared for use cases where multisignatures for large sets of signers are required but the size of concatenated Ed25519 signatures is too disadvantageous. Cross chain transactions for a trustless interoperability solution are likely candidates for such use cases.
With the BLS variant we choose, several signatures of the same message can be aggregated into a single compact signature with a size of 96 bytes. If we consider, for example, a transaction that requires signatures from 68 active validators, concatenated Ed25519 signatures would sum up to more than 4.35 kB which is about 45 times larger than an aggregate BLS signature.
Very recent advancements have pushed the BLS signature scheme to a state that gives sufficient confidence in the theory of BLS signatures and in its implementations: The IETF standardization process was initiated and driven forward, several implementations were developed, matured and partially audited, and last but not least, the Ethereum2 Beacon Chain that started running recently adopted BLS, which results in real-world usage of both the BLS scheme as specified in the latest standard draft (version 4 at the time of writing) and the BLS implementations. Note that filecoin is using BLS signatures as well. However, their specification is based on an outdated BLS specification draft.
The BLS signature scheme as specified in the IETF draft BLS Signatures draft-irtf-cfrg-bls-signature-04 is used. More specifically, the ciphersuite BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_ is chosen. This ciphersuite uses the proof of possession scheme and the minimal-pubkey-size variant. Public keys use 48 bytes and signatures 96 bytes.
The ciphersuite exposes the following functions:
KeyGen
SkToPk
Aggregate
Sign
(identical toCoreSign
)Verify
(identical toCoreVerify
)AggregateVerify
(identical toCoreAggregateVerify
)FastAggregateVerify
PopProve
PopVerify
The function AggregateVerify
is, however, not used here. How and when the remaining functions are used is specified in the following subsection.
A secret key is created using KeyGen
. The input for KeyGen
must be an infeasible to guess octet string of length at least 32. See the appendix for a recommendation on how to choose this input and on key management.
The public key for a secret key sk
is created by SkToPk(sk)
.
(Note: The following function definitions are superseded by the ones in LIP 0062.)
Let message
be a binary message, tag
the correct message tag for message
as specified in LIP 0037, chainID
the correct chain ID of the chain and sk
a secret key. Then, the signature is computed by signBLS(sk, tag, chainID, message)
as defined below. The resulting signature sig
in combination with the message message
and the matching public key pk
is verified by verifyBLS(pk, tag, chainID, message, sig)
. In the following, let tagMessage
be the function defined in LIP 0037.
def signBLS(sk: bytes, tag: bytes, chainID: bytes, message: bytes) -> bytes:
taggedMessage = tagMessage(tag, chainID, message)
return Sign(sk, taggedMessage)
def verifyBLS(pk: bytes, tag: bytes, chainID: bytes, message: bytes, sig: bytes) -> bool:
taggedMessage = tagMessage(tag, chainID, message)
return Verify(pk, taggedMessage, sig) == VALID
In order to use a BLS keypair (sk, pk)
for on-chain signatures, the public key pk
of the keypair must first be registered on-chain via some transaction. Otherwise, every transaction or block that needs to verify a signature for pk
via Verify
or FastAggregateVerify
must be rejected.
The Lisk protocol could contain several transaction types that perform such a registration. In particular, there could be different registration transactions for different keys, e.g, one for validator public keys and one for public keys of regular accounts. This LIP does not specify any registration transactions. Such transaction types must be defined in separate LIPs. In the following, we just assume there exists such a transaction type which we call register public key transaction. To register the public key of the key pair (sk, pk)
by a register public key transaction, registerPublicKeyTransaction
, the transaction must contain pk
and a proof, prf
, generated by PopProve(sk)
. If prf
does not satisfy PopVerify(pk, prf) == VALID
, then registerPublicKeyTransaction
is invalid and must be rejected. Once registerPublicKeyTransaction
is included, transactions and blocks that require to have a valid signature for pk
can be included in the blockchain.
Example (validator registration): Validators will be required to register their BLS public key on-chain, which may be included in the validator registration transactions. Hence, a validator registration transaction needs to contain the BLS public key, pk
, and a proof, prf
, generated by PopProve(sk)
, where sk
is the matching secret key. During the validation of the validator registration transaction, it must be checked that PopVerify(pk, prf)
returns VALID
.
We only consider signature aggregation for the case where several signatures for the same message are aggregated.
Each aggregate signature needs to be accompanied by some information that specifies the set of public keys that correspond to the aggregate signature. Here, this is realized using a bitmap. Assume that keyList
is a list that includes all potential public keys that could participate in the signature aggregation. The entries must be pairwise distinct. Moreover, let pubKeySignaturePairs
be a list of pairs of public keys and signatures where all signatures belong to the same message, and all public keys are unique and contained in keyList
. Then, the corresponding aggregate signature and bitmap can be computed via createAggSig(keysList, pubKeySignaturePairs)
as in the pseudo code below. To verify if a signature is an aggregate signature of a binary message message
, the function verifyAggSig
can be used. verifyAggSig(keysList, aggregationBits, signature, tag, chainID, message)
returns True
if and only if signature
is an aggregate signature of the message message
for the message tag tag
, the chain identifier chainID
and for the public keys in keyList
defined by aggregationBits
.
def createAggSig(keysList: list[bytes], pubKeySignaturePairs: list[tuple[bytes, bytes]]) -> tuple[bytes, bytes]:
aggregationBits = byte string of length ceiling(length(keyList), 8) with all bytes set to 0
signatures = []
for pair in pubKeySignaturePairs:
signatures.append(pair[1])
index = keysList.index(pair[0])
# set bit at position index to 1 in aggregationBits
aggregationBits[index // 8] |= 1 << (index % 8)
signature = Aggregate(signatures)
return (aggregationBits, signature)
Here, ceiling is an auxiliary function defined below.
def verifyAggSig(keysList: list[bytes], aggregationBits: bytes, signature: bytes, tag: bytes, chainID: bytes, message: bytes) -> bool:
taggedMessage = tagMessage(tag, chainID, message)
keys = []
if len(aggregationBits) != ceiling(len(keysList), 8):
return False
# ensure that the bits not corresponding to a key in keysList are all zero
if len(keysList) % 8 != 0 and not (aggregationBits[-1] >> (len(keysList) % 8) == 0):
return False
for i in range(8 * len(aggregationBits)):
# if i-th bit of aggregationBits == 1
if (aggregationBits[i // 8] >> (i % 8)) & 1:
keys.append(keysList[i])
return FastAggregateVerify(keys, taggedMessage, signature) == VALID
(Note: The definition of the function verifyAggSig
is superseded by the one in LIP 0062.)
If one wants to additionally validate that the participating public keys satisfy a certain weight threshold, the function verifyWeightedAggSig
can be used. The function takes additionally a list of weights, weights
, where the i-th entry specifies the weight for the i-th public key in keysList
and a weight threshold threshold
.
def verifyWeightedAggSig(keysList: list[bytes], aggregationBits: bytes, signature: bytes, tag: bytes, chainID: bytes, weights: list[int], threshold: int, message: bytes) -> bool:
taggedMessage = tagMessage(tag, chainID, message)
keys = []
weightSum = 0
if len(aggregationBits) != ceiling(len(keysList), 8):
return False
# ensure that the bits not corresponding to a key in keysList are all zero
if len(keysList) % 8 != 0 and not (aggregationBits[-1] >> (len(keysList) % 8) == 0):
return False
for i in range(8 * len(aggregationBits)):
# if i-th bit of aggregationBits == 1
if (aggregationBits[i // 8] >> (i % 8)) & 1:
keys.append(keysList[i])
weightSum += weights[i]
if weightSum < threshold:
return False
return FastAggregateVerify(keys, taggedMessage, signature) == VALID
(Note: The definition of the function verifyWeightedAggSig
is superseded by the one in LIP 0062.)
Note that the public keys in pubKeySignaturePairs
need to be distinct when calling createAggSig.
Otherwise, validation via verifyAggSig
and verifyWeightedAggSig
will fail.
The IETF draft restricts the usage of FastAggregateVerify
to public keys for which there was a valid proof of possession. Moreover, the behaviour of FastAggregateVerify
is not well defined for public keys that cannot be deserialized to curve points. Here, we amend the rules for FastAggregateVerify
by the following:
- If any of the input public keys is the byte string of length 48 in which all bytes are set to zero, then
FastAggregateVerify
must returnINVALID
. `
Returns the ceiling of the division between two positive integers.
def ceiling(x: uint32,y: uint32) -> uint32:
if y == 0:
raise Exception('Can not divide by 0.')
return (x + y - 1) // y
We choose the variant minimal-pubkey-size because this one is used in Ethereum2 and filecoin and therefore the only variant that found considerable adoption. This means in particular that only the minimal-pubkey-size functionality of BLS libraries is significantly used and tested in practice and can be relied on.
We use the proof of possession scheme as we only need the use case of aggregating signatures of the same message, and this scheme allows us to use FastAggregateVerify
for this case. FastAggregateVerify
requires only two pairing operations whereas AggregateVerify
requires n
+1 pairing operations where n
is the number of individual signatures that are aggregated. Note that pairing operations are very expensive.
Using FastAggregateVerify
without requiring proofs of possession is insecure as it allows rogue key attacks. See this blog post for how rogue key attacks work for BLS signatures. To see why simply signing the public key is not a sufficient proof of possession method that defends against powerful attackers (chosen message attack model), see section 4.3 of this paper.
Lisk Core v4 poses an exception to the rule that only public keys for which a valid proof of possession exists can be used. More precisely, during the starting phase of Lisk Core v4, there will be validators with the invalid BLS public key where all 48 bytes are set to zero. See here for more details why this is needed. Due to this, it can happen that Lisk Core v4 nodes or even sidechain nodes must evaluate FastAggregateVerify
where one of the input public keys is the one where all 48 bytes are set to zero. Although the IETF draft does not specify the behavior, the used BLS-library in the Lisk SDK 6 does actually return INVALID/False
for this specific case. Therefore, it was considered easier to add this extra rule that matches with the implementation to this LIP instead of adding additional rules in the SDK that prevent that FastAggregateVerify
is called with the key where all 48 bytes are set to zero.
Notice that only during the bootstrap period of Lisk Core v4 and the following 103 rounds, an active vaildator can have this specific invalid BLS public key. After this phase, those validators cannot be elected as they are banned. Consequently, evaluating FastAggregateVerify
with an invalid public key will also not happen anymore soon after this phase.
This LIP is purely informational. Therefore, it does not imply any incompatibilities.
We propose two ways to manage the keys: using a secret recovery phrase and storing the encrypted secret key. Moreover, we discuss below for which use cases the methods are preferred.
This approach is similar to the key derivation method for the EdDSA account key pair in Lisk.
To create a new key pair, a secret recovery phrase is created according to the BIP 39 specifications, where an initial entropy of 32 bytes is used. In Node.js, this initial entropy can be created, for example, via crypto.randomBytes. The resulting secret recovery phrase consists of 24 words.
The secret recovery phrase is used as the input for KeyGen
to derive the secret key, where the secret recovery phrase is treated as a single ASCII-encoded string with the space symbol (0x20) between two words. The user needs to remember or store safely the secret recovery phrase.
If the secret key is needed on a remote server, the encrypted secret recovery phrase must be stored on the server. The secret recovery phrase should be encrypted by AES-256-GCM, and the encryption key should be derived by Argon2d. The password used to derive the encryption key should conform to common guidelines for strong passwords. On the user interface level, the user should be warned otherwise.
To create a new key pair, an initial randomness of at least 32 bytes is created, e.g., via crypto.randomBytes. This randomness is used as the input for KeyGen
to derive the secret key. The secret key is encrypted via AES-256-GCM, where the encryption key is derived by Argon2d. The password used to derive the encryption key should conform to common guidelines for strong passwords. On the user interface level, the user should be warned otherwise. The user needs to store the encrypted secret key (and ideally backs up the encrypted key) and needs to remember or store the password safely.
Using a secret recovery phrase is suitable for users that need their key pair only on local machines, e.g., for singing transactions. Users only need to remember or securely store the secret recovery phrase for this approach. Storing the encrypted secret key is suitable for users that need the key pair only on some remote server, e.g., a validator node on a remote data center. If the key pair is needed on local machines and on remote servers, there is a tradeoff between the two approaches. The first one requires to remember or store secretly a secret recovery phrase and a password, but does not require to store and backup any encrypted data locally. The second one requires to store and backup an encrypted file, but needs to remember or secretly store only one password.
The Eth2.0 specification tests for Sign
should be used. Additionally, the following case for a secret key that is non-zero but zero modulo the group order must hold (note that this must hold for any message):
# sk equals 2*r where r is order of the groups G1 and G2
sk = 0xe7db4ea6533afa906673b0101343b00aa77b4805fffcb7fdfffffffe00000002
message = 0xabababababababababababababababababababababababababababababababab
Sign(sk, message) == INVALID
The Eth2.0 specification tests for Verify
should be used. Additionally, the following cases must hold:
# pk does not represent a point on the curve E1
pk = 0xa53d21a4cfd562c469cc81514d4ce5a6b577d8403d32a394dc265dd190b47fa9f829fdd7963afdf972e5e77854051f6f
message = 0xabababababababababababababababababababababababababababababababab
sig = 0xae82747ddeefe4fd64cf9cedb9b04ae3e8a43420cd255e3c7cd06a8d88b7c7f8638543719981c5d16fa3527c468c25f0026704a6951bde891360c7e8d12ddee0559004ccdbe6046b55bae1b257ee97f7cdb955773d7cf29adf3ccbb9975e4eb9
Verify(pk, message, sig) == INVALID
# pk represents a point on the curve E1 but is NOT an element in the group G1
pk = 0x960003aaf1632b13396dbad518effa00fff532f604de1a7fc2082ff4cb0afa2d63b2c32da1bef2bf6c5ca62dc6b72f9c
message = 0xabababababababababababababababababababababababababababababababab
sig = 0xae82747ddeefe4fd64cf9cedb9b04ae3e8a43420cd255e3c7cd06a8d88b7c7f8638543719981c5d16fa3527c468c25f0026704a6951bde891360c7e8d12ddee0559004ccdbe6046b55bae1b257ee97f7cdb955773d7cf29adf3ccbb9975e4eb9
Verify(pk, message, sig) == INVALID
The Eth2.0 specification tests for Aggregate should be used. Moreover, the following additional cases should be covered:
# signatures for the message
# 0xabababababababababababababababababababababababababababababababab
# and the secret keys sk1=1 and sk2=r-1 where r is order of the groups G1 and G2,
# i.e., r = 0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001.
# Hence, the aggregate signature must be the identity element in G2.
signatures = [
0x979451d90ade914f7a6ffc5062914af990af297abdebf81dcebcaff93a5cb959e7f5db624bc8abb8cdb2660374c86a350bc0f071f2d0655a5edbf6b9208a6649d3309b8692d2f55bde74c52cc2de0fed2bb60b4c45935b11c32827da1b80cb8f,
0xb79451d90ade914f7a6ffc5062914af990af297abdebf81dcebcaff93a5cb959e7f5db624bc8abb8cdb2660374c86a350bc0f071f2d0655a5edbf6b9208a6649d3309b8692d2f55bde74c52cc2de0fed2bb60b4c45935b11c32827da1b80cb8f
]
Aggregate(signatures) == 0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
# one signature that does not represent a curve point on E2
signatures = [0xc10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000]
Aggregate(signatures) == INVALID
# third signature does not represent a curve point on E2
signatures = [
0xb6ed936746e01f8ecf281f020953fbf1f01debd5657c4a383940b020b26507f6076334f91e2366c96e9ab279fb5158090352ea1c5b0c9274504f4f0e7053af24802e51e4568d164fe986834f41e55c8e850ce1f98458c0cfc9ab380b55285a55,
0xb23c46be3a001c63ca711f87a005c200cc550b9429d5f4eb38d74322144f1b63926da3388979e5321012fb1a0526bcd100b5ef5fe72628ce4cd5e904aeaa3279527843fae5ca9ca675f4f51ed8f83bbf7155da9ecc9663100a885d5dc6df96d9,
0x948a7cb99f76d616c2c564ce9bf4a519f1bea6b0a624a02276443c245854219fabb8d4ce061d255af5330b078d5380681751aa7053da2c98bae898edc218c75f07e24d8802a17cd1f6833b71e58f5eb5b94208b4d0bb3848cecb075ea21bffff
]
Aggregate(signatures) == INVALID
The Eth2.0 specification tests for FastAggregateVerify
should be used. However, an implementation for this proposal is not required to fulfill the test for infinity public key (one public key is the identity point of the curve). The behaviour for this input is undefined: The specifications for the proof of possession scheme, as well as the section "Public Key Registration and Proof of Possession" are demanding that PopVerify
must return VALID
for all public keys used as input for FastAggregateVerify
, which is not the case for the mentioned Eth2.0 test. Due to our additional rule for FastAggregateVerify
, the following cases should be covered:
# Invalid case: first public key is the special invalid public key used in Lisk Core v4.
pks = [
0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,
0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a,
0xb301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81
]
message = 0x5656565656565656565656565656565656565656565656565656565656565656
signature = 0x912c3615f69575407db9392eb21fee18fff797eeb2fbe1816366ca2a08ae574d8824dbfafb4c9eaa1cf61b63c6f9b69911f269b664c42947dd1b53ef1081926c1e82bb2a465f927124b08391a5249036146d6f3f1e17ff5f162f779746d830d1
FastAggregateVerify(pks, message, signature) == INVALID
# Invalid case: one of the public keys, but not the first one, is the special invalid public key used in Lisk Core v4.
pks = [
0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a,
0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,
0xb301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81
]
message = 0x5656565656565656565656565656565656565656565656565656565656565656
signature = 0x912c3615f69575407db9392eb21fee18fff797eeb2fbe1816366ca2a08ae574d8824dbfafb4c9eaa1cf61b63c6f9b69911f269b664c42947dd1b53ef1081926c1e82bb2a465f927124b08391a5249036146d6f3f1e17ff5f162f779746d830d1
FastAggregateVerify(pks, message, signature) == INVALID
# Invalid case: all public keys are the special invalid public key used in Lisk Core v4.
pks = [
0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,
0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
]
message = 0x5656565656565656565656565656565656565656565656565656565656565656
signature = 0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
FastAggregateVerify(pks, message, signature) == INVALID
Moreover, the following case should be covered:
# Invalid case: aggregate public key is identity element.
# The public keys are derived from the secret keys sk1=1 and sk2=r-1 where r
# is the order of the groups G1 and G2. The signature is the correct aggregate
# signature of the given message for the two keys. The public keys are valid,
# but their sum (the aggregate public key) is the identity element in G1.
# Therefore, signature validation must fail.
pks = [
0x97f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb,
0xb7f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb
]
message = 0xabababababababababababababababababababababababababababababababab
signature = 0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
FastAggregateVerify(pks, message, signature) == INVALID
sk = 0x263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3
SkToPk(sk) == 0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a
sk = 0x4dcffb43f4730ddb9364ef30a6b9b3e0343582e5df6bcd315f201cb3234adae3
SkToPk(sk) == 0xa6b6a639f7fa0b64ad3a93be965e9cc34e1d9d0f0427c14c38fc80934a937c5fa745a3cb285f64d4d1c06d0825504488
sk = 0x18a4b157ca6d83fe3081bbf6a63edbacf543a1c2a4b0befe68f912597f2c71c1
SkToPk(sk) == 0x884b52f84e801d2453edb023928c79125a5e4384c108dd8f17b7f2a20772c7dc4b9635602937df1b87d8b7284870c932
sk = 0x47b8192d77bf871b62e87859d653922725724a5c031afeabc60bcef5ff665138
SkToPk(sk) == 0xb301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81
sk = 0x258787ef728c898e43bc76244d70f468c9c7e1338a107b18b42da0d86b663c26
PopProve(sk) == 0x84f709159435f0dc73b3e8bf6c78d85282d19231555a8ee3b6e2573aaf66872d9203fefa1ef700e34e7c3f3fb28210100558c6871c53f1ef6055b9f06b0d1abe22ad584ad3b957f3018a8f58227c6c716b1e15791459850f2289168fa0cf9115
sk = 0x47b8192d77bf871b62e87859d653922725724a5c031afeabc60bcef5ff665138
PopProve(sk) == 0x88bb31b27eae23038e14f9d9d1b628a39f5881b5278c3c6f0249f81ba0deb1f68aa5f8847854d6554051aa810fdf1cdb02df4af7a5647b1aa4afb60ec6d446ee17af24a8a50876ffdaf9bf475038ec5f8ebeda1c1c6a3220293e23b13a9a5d26
sk = 0x263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3
PopProve(sk) == 0xb803eb0ed93ea10224a73b6b9c725796be9f5fefd215ef7a5b97234cc956cf6870db6127b7e4d824ec62276078e787db05584ce1adbf076bc0808ca0f15b73d59060254b25393d95dfc7abe3cda566842aaedf50bbb062aae1bbb6ef3b1f77e1
Valid cases:
pk = 0xb301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81
proof = 0x88bb31b27eae23038e14f9d9d1b628a39f5881b5278c3c6f0249f81ba0deb1f68aa5f8847854d6554051aa810fdf1cdb02df4af7a5647b1aa4afb60ec6d446ee17af24a8a50876ffdaf9bf475038ec5f8ebeda1c1c6a3220293e23b13a9a5d26
PopVerify(pk, proof) == VALID
pk = 0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a
proof = 0xb803eb0ed93ea10224a73b6b9c725796be9f5fefd215ef7a5b97234cc956cf6870db6127b7e4d824ec62276078e787db05584ce1adbf076bc0808ca0f15b73d59060254b25393d95dfc7abe3cda566842aaedf50bbb062aae1bbb6ef3b1f77e1
PopVerify(pk, proof) == VALID
Invalid cases:
# public key is identity point in the group G1
pk = 0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
proof = 0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
PopVerify(pk, proof) == INVALID
# pk does not represent a point on the curve E1
pk = 0xa53d21a4cfd562c469cc81514d4ce5a6b577d8403d32a394dc265dd190b47fa9f829fdd7963afdf972e5e77854051f6f
proof = 0x88bb31b27eae23038e14f9d9d1b628a39f5881b5278c3c6f0249f81ba0deb1f68aa5f8847854d6554051aa810fdf1cdb02df4af7a5647b1aa4afb60ec6d446ee17af24a8a50876ffdaf9bf475038ec5f8ebeda1c1c6a3220293e23b13a9a5d26
PopVerify(pk, proof) == INVALID
# pk represents a point on the curve E1 but is NOT an element in the group G1
pk = 0x960003aaf1632b13396dbad518effa00fff532f604de1a7fc2082ff4cb0afa2d63b2c32da1bef2bf6c5ca62dc6b72f9c
proof = 0x88bb31b27eae23038e14f9d9d1b628a39f5881b5278c3c6f0249f81ba0deb1f68aa5f8847854d6554051aa810fdf1cdb02df4af7a5647b1aa4afb60ec6d446ee17af24a8a50876ffdaf9bf475038ec5f8ebeda1c1c6a3220293e23b13a9a5d26
PopVerify(pk, proof) == INVALID
# proof is not a point on the curve E2 (tampered proof)
pk = 0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a
proof = 0xb803eb0ed93ea10224a73b6b9c725796be9f5fefd215ef7a5b97234cc956cf6870db6127b7e4d824ec62276078e787db05584ce1adbf076bc0808ca0f15b73d59060254b25393d95dfc7abe3cda566842aaedf50bbb062aae1bbb6ef3b1fffff
PopVerify(pk, proof) == INVALID
# proof is a point on the curve E2 but not the matching proof for pk
pk = 0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a
proof = 0x88bb31b27eae23038e14f9d9d1b628a39f5881b5278c3c6f0249f81ba0deb1f68aa5f8847854d6554051aa810fdf1cdb02df4af7a5647b1aa4afb60ec6d446ee17af24a8a50876ffdaf9bf475038ec5f8ebeda1c1c6a3220293e23b13a9a5d26
PopVerify(pk, proof) == INVALID
sk = 0x263dbd792f5b1be47ed85f8938c0f29586af0d3ac7b977f21c278fe1462040e3
tag = 0x4c534b5f54585f # ASCII-encoding of "LSK_TX_"
chainID = 0x00000000
message = 0xbeaf
signBLS(sk, tag, chainID, message) == 0xb1a70d38d539d469ff78e93113767103a8ded1974877004b3291be40eaee0e5f171b7a5c7dd4bb6ca8ce6dadf6b9668d0fd7662d909dd59dc8f65cd8fdf436c2a3fa107ed4f7f2682fb6610bd7e2eae3fba2995abcd4697c2b8f23d252354ca5
pk = 0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a
tag = 0x4c534b5f54585f # ASCII-encoding of "LSK_TX_"
chainID = 0x00000000
message = 0xbeaf
sig = 0xb1a70d38d539d469ff78e93113767103a8ded1974877004b3291be40eaee0e5f171b7a5c7dd4bb6ca8ce6dadf6b9668d0fd7662d909dd59dc8f65cd8fdf436c2a3fa107ed4f7f2682fb6610bd7e2eae3fba2995abcd4697c2b8f23d252354ca5
verifyBLS(pk, tag, chainID, message, sig) == True
We use the following list of keys for all test below:
KEYS_LIST = [
0x9998f02d85e3851a430333350ed6cc1c0afbd72ee52cf8ad2f23d394f3937bfdc92e056dce713b9d45dac7b106d82883,
0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a,
0x8f116ba0b305fb734405dd0968e255ad06a34d0cacfeece4c320502824da4a2ff90a978bfcffa1206ecae27f62bac645,
0xb301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81,
0xb53d21a4cfd562c469cc81514d4ce5a6b577d8403d32a394dc265dd190b47fa9f829fdd7963afdf972e5e77854051f6f,
0xa6b6a639f7fa0b64ad3a93be965e9cc34e1d9d0f0427c14c38fc80934a937c5fa745a3cb285f64d4d1c06d0825504488,
0xa4aa20eedb651b7855ee38ce16f59a263346fc383dd9603ac219aaed166ebfe09d460ebbbb7ea89e71c70d48e06efd1a,
0x95324a8c4a890e8c1e83c96c6c639254937c9c9cee789556606744b07e98292e292c8c150efd9506b0b5547fea3fdf9f,
0xa424801164381bbfc0b20c1807ce43a12bb012e47deb11b2a3a273dd82ca9fa6364e2f2b8d6c89bc576da89a04d5118f
]
pubKeySignaturePairs = [
[
pk = 0xa491d1b0ecd9bb917989f0e74f0dea0422eac4a873e5e2644f368dffb9a6e20fd6e10c1b77654d067c0618f6e5a7f79a,
sig = 0x91347bccf740d859038fcdcaf233eeceb2a436bcaaee9b2aa3bfb70efe29dfb2677562ccbea1c8e061fb9971b0753c240622fab78489ce96768259fc01360346da5b9f579e5da0d941e4c6ba18a0e64906082375394f337fa1af2b7127b0d121
],
[
pk = 0xb301803f8b5ac4a1133581fc676dfedc60d891dd5fa99028805e5ea5b08d3491af75d0707adab3b70c6a6a580217bf81,
sig = 0x9674e2228034527f4c083206032b020310face156d4a4685e2fcaec2f6f3665aa635d90347b6ce124eb879266b1e801d185de36a0a289b85e9039662634f2eea1e02e670bc7ab849d006a70b2f93b84597558a05b879c8d445f387a5d5b653df
],
[
pk = 0xb53d21a4cfd562c469cc81514d4ce5a6b577d8403d32a394dc265dd190b47fa9f829fdd7963afdf972e5e77854051f6f,
sig = 0xae82747ddeefe4fd64cf9cedb9b04ae3e8a43420cd255e3c7cd06a8d88b7c7f8638543719981c5d16fa3527c468c25f0026704a6951bde891360c7e8d12ddee0559004ccdbe6046b55bae1b257ee97f7cdb955773d7cf29adf3ccbb9975e4eb9
]
]
createAggSig(KEYS_LIST, pubKeySignaturePairs) == [0x1a00, 0x9712c3edd73a209c742b8250759db12549b3eaf43b5ca61376d9f30e2747dbcf842d8b2ac0901d2a093713e20284a7670fcf6954e9ab93de991bb9b313e664785a075fc285806fa5224c82bde146561b446ccfc706a64b8579513cfc4ff1d930]
pubKeySignaturePairs = [
[
pk = 0xa4aa20eedb651b7855ee38ce16f59a263346fc383dd9603ac219aaed166ebfe09d460ebbbb7ea89e71c70d48e06efd1a,
sig = 0x874fa8727ec92caf4981483e59ac47a0df33161809867ea8a3a37799ce4bcea6e8ff9b15038e327ddbcb4248e795ca02028d3af4ad410566089f50e54a065ea96c070f59717d6acd897805a33de6cb474c228d05a174d76613a8aa21d41d8774
],
[
pk = 0xa424801164381bbfc0b20c1807ce43a12bb012e47deb11b2a3a273dd82ca9fa6364e2f2b8d6c89bc576da89a04d5118f,
sig = 0xb1a4ab4d97409678dea8df03f00e5d2b25cdb70122b9b1b885e49b99bb00924dd907d3d3c29f06110eff8046515192d111fcdee246c280453ff7e21ae7696ae4c9fb7c2f704875b41e42dd5b6556ad938abb6dfec718427db4b97c9a56c4f651
]
]
createAggSig(KEYS_LIST, pubKeySignaturePairs) == [0x4001, 0x9849a1ea288435279886b760707c14f026f72a93f2b5a5c47f9a5fd419c4ee3f33e5fa288035370f55274e23838dcb0d11a50db2febd20a4080eb9e67b1900e9ba5079696b9f6061e84f7ecdfe2533b2662d7cbb0d73c29c38cd4ad88437cc11]
aggregationBits = 0x4001
signature = 0x9849a1ea288435279886b760707c14f026f72a93f2b5a5c47f9a5fd419c4ee3f33e5fa288035370f55274e23838dcb0d11a50db2febd20a4080eb9e67b1900e9ba5079696b9f6061e84f7ecdfe2533b2662d7cbb0d73c29c38cd4ad88437cc11
tag = 0x4c534b5f43455f # ASCII-encoding of "LSK_CE_"
chainID = 0x00000000
message = 0xbeaf
verifyAggSig(KEYS_LIST, aggregationBits, signature, tag, chainID, message) == True