This is the first commit from PR 17977 that uses the new functionality and interfaces
in libsecp256k1. You may need to make clean && ./configure && make for the build to
Schnorr signatures are a
digital signature scheme that can be constructed over any group where the
discrete log problem is hard. It’s a different signature scheme from ECDSA,
which is used in Bitcoin today, but there are certain similarities between the
We won’t go into the construction of Schnorr signatures or the motivation for
adding them to Bitcoin, but it’d be useful to have a high-level understanding of
those things to motivate why we’re making these changes. BIP
both the motivation and the construction of these signatures.
We also won’t go into the implementation of signing and verifying the
signatures themselves. That’s all hidden away inside the libsecp library. The
commits from PR 17977 pull in the required changes to libsecp. If you want to
dig deeper into the implementation, lipsecp PR
558 is where the new
cryptographic code is implemented.
Instead, we’re going to look at the new interface provided by libsecp for
verifying Schnorr signatures, and how it’s integrated into Bitcoin Core.
The first thing to note about the Schnorr signature interface is that
are 32 bytes. That’s unlike pubkeys in our ECDSA implementation,
which are 33 or 65 bytes (depending on whether they’re compressed or
uncompressed). BIP 340 public keys can be 32 bytes because the y-value
or the point is implicit. See the Implicit Y coordinates and Public Key
Generation sections of BIP
340 for full
The new XOnlyPubKey object is the lowest-level object we’ll use for signature
validation. It’s constructed with a uint256, and then the function
VerifySchnorr() is called with the 32 bytes hash of the message that was
signed (this hash can sometimes just be called the ‘message’, or in the case of
signing a Bitcoin transaction the ‘sighash’), and the 64 byte signature.
The XOnlyPubKey object is used within a hierarchy of signature checker classes,
which are used by the higher-level code to check signatures:
At the base is
which provides three (virtual) public methods: CheckSig(),
CheckLockTime() and CheckSequence(). This PR adds CheckSchnorrSig().
Above that is
(which is actually a template that’s instantiated as
TransactionSignatureChecker or MutableTransactionSignatureChecker depending
on whether it’s being used with a CTransaction or CMutableTransaction).
GenericTransactionSignatureChecker overrides the virtual CheckSig() and
CheckSchnorrSig() methods with implementations of signature verification.
And above TransactionSignatureChecker is a
which uses the signature cache (calling
CachingTransactionSignatureChecker::Verify____Signature() more than once on
the same transaction won’t result in the same signatures being validated
<fjahr> The "Implicit Y coordinates" section in BIP340 explains it: Every X coordinate has 2 two Y coordinates. There are different options on how to determine which Y to use but the Y which is a quadratic residue was chosen.
<jnewbery> a public key in elliptic curve cryptography is a point, but because the curve is mirrored in the x-axis, as long as we all agree a scheme to determine which of the 2 possible y values to choose, then we don't actually need to explicitly state which y value we're using
<fjahr> 65 bytes includes a different sighash type (not SIGHASH_DEFAULT) in the last byte. It gets stripped and the 64 bytes are used to check sig. If its 64 bytes the default is used automatically. 65 bytes with SIGHASH_DEFAULT is not allowed to protect against malleability.
<willcl_ark> <sipa> pinheadmz: you can do either; have a 64-byte one with implicit sighash 0, or explicitly make a 65-byte sig with hashtype 1; their semantics are the same... but the signature will still differ because the hash commits to the actual hashtype value
<thomasb06> for real numbers, the equivalent of quadratic residue is "is the number positive, or negative". If the number is positive, it has a square root. If it's negative, it hasn't. In F_p, half numbers are square roots and half are not. The Legendre symbole says if it does or not.
<michaelfolkson> "The benchmark was repeatedly testing the same constant input, which apparently was around 2.5x faster than the average speed. It is a variable-time algorithm, so a good variation of inputs matters."
<jnewbery> sipa: lots to digest there. My very rudimentary understanding was that to lift an x co-ordinate onto a point, the last step was squaring, so you'd always end with a quadratic residue. Is that relevant here?