Skip to content

Commit 9548f31

Browse files
authored
schnorr: Adds Schnorr implementation following draft-ERC
1 parent eff4232 commit 9548f31

31 files changed

+1598
-958
lines changed

.github/workflows/solc-version-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ jobs:
2121
- name: Run forge tests against lowest and highest supported solc version
2222
run: >
2323
forge test --use 0.8.16 &&
24-
forge test --use 0.8.26
24+
forge test --use 0.8.27

Makefile

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ clean: ## Clean build artifacts
1919

2020
.PHONY: test
2121
test: ## Run full test suite
22-
@forge test
22+
@forge test --show-progress
2323

2424
.PHONY: test-intense
2525
test-intense: ## Run full test suite with intense fuzzing
26-
@FOUNDRY_PROFILE=intense forge test
26+
@FOUNDRY_PROFILE=intense forge test --show-progress
2727

2828
.PHONY: test-summary
2929
test-summary: ## Print summary of test suite
30-
@forge test --summary
30+
@forge test --summary --show-progress
3131

3232
.PHONY: coverage
3333
coverage: ## Update coverage report and open lcov web interface
@@ -62,6 +62,18 @@ examples: ## Run examples
6262
@echo "##"
6363
@echo "########################################"
6464
@forge script examples/secp256r1/Secp256r1.sol:Secp256r1Example -v
65+
@echo "########################################"
66+
@echo "##"
67+
@echo "## ECDSA on secp56k1"
68+
@echo "##"
69+
@echo "########################################"
70+
@forge script examples/secp256k1/signatures/ECDSA.sol:ECDSAExample -v
71+
@echo "########################################"
72+
@echo "##"
73+
@echo "## Schnorr (ERC-XXX)"
74+
@echo "##"
75+
@echo "########################################"
76+
@forge script examples/secp256k1/signatures/Schnorr.sol:SchnorrExample -v
6577

6678
.PHONY: fmt
6779
fmt: ## Format project

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ $ forge install verklegarden/crysol
5353
## Examples
5454

5555
Several examples are provided in [`examples/`](./examples), such as:
56-
- secure key pair and Ethereum address creation
56+
- secure key pair and Ethereum address generation
5757
- secp256k1 point arithmetic
5858
- Schnorr and ECDSA signature creation and verification
5959

docs/Intro.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,30 @@ modifier vmed() {
3030
// forgefmt: disable-end
3131
// ~~~~~~~~~~~~~~~~~~~~~~~
3232
```
33+
34+
## TODO: Signature Rules
35+
36+
- Malleable signatures are deemed invalid
37+
- For ECDSA malleable signature if `s > Q/2` which is in sync with ecosystem
38+
- Only 32 byte digests are signed, never byte strings
39+
- It is always assumed that the digest is a keccak256 hash digest
40+
- The default `sign` function MUST only sign domain separated messages
41+
- For ECDSA using "Ethereum Signed Message" as defined via `eth_sign`
42+
- For Schnorr its part of ERC-XXX
43+
- There are `signRaw` functions that MUST NOT domain separate user input digests
44+
- Usage is discouraged
45+
- User input is called `digest`, which is domain separated to message `m`, wich is signed
46+
47+
- Terminology:
48+
- A user wants to sign byte string `message`
49+
- The `message` is hashed via keccak256 to `digest`
50+
- The `digest` is domain separated to `m`
51+
52+
## TODO: Sanity Check Rules
53+
54+
- De/Serialization functions MUST revert if object is invalid/insane
55+
- Example: It MUST NOT be possible to serialize an invalid public key
56+
- Example: It MUST NOT be possible to deserialize a malleable ECDSA signature
57+
- Type conversion functions MUST NOT revert if object is invalid/insane
58+
- Example: It MUST be possible to transform an invalid public key to an point
59+
- Example: It MUST be possible to transform an insance Schnorr signature to a compressed Schnorr signature

docs/secp256k1/signatures/Schnorr.md

Lines changed: 0 additions & 164 deletions
This file was deleted.

examples/secp256k1/signatures/ECDSA.sol

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,43 @@ contract ECDSAExample is Script {
3838
using ECDSA for Signature;
3939

4040
function run() public {
41-
bytes memory message = bytes("crysol <3");
42-
43-
// Create new cryptographically sound secret key.
41+
// Create new cryptographically sound secret key and respective
42+
// public key and address.
4443
SecretKey sk = Secp256k1Offchain.newSecretKey();
45-
// assert(sk.isValid());
44+
PublicKey memory pk = sk.toPublicKey();
45+
address addr = pk.toAddress();
46+
47+
// Create digest of the message to sign.
48+
//
49+
// crysol's sign() functions only accept bytes32 digests to enforce
50+
// static payload size.
51+
bytes32 digest = keccak256(bytes("crysol <3"));
52+
53+
// Note that crysol's sign() function domain separates input digests.
54+
// The actual message being signed can be constructed via:
55+
bytes32 m = ECDSA.constructMessageHash(digest);
4656

47-
// Sign message via ECDSA.
48-
Signature memory sig = sk.sign(message);
57+
// Sign digest via ECDSA.
58+
Signature memory sig = sk.sign(digest);
4959
console.log("Signed message via ECDSA, signature:");
5060
console.log(sig.toString());
5161
console.log("");
5262

63+
// It's also possible to use the low-level signRaw() function to not
64+
// domain separate the input digest.
65+
// However, usage is discouraged.
66+
Signature memory sig2 = sk.signRaw(m);
67+
68+
// Note that crysol uses RFC-6979 to construct deterministic ECDSA
69+
// nonces and thereby signatures. Therefore, the two signatures are
70+
// expected to be equal.
71+
assert(sig.v == sig2.v);
72+
assert(sig.r == sig2.r);
73+
assert(sig.s == sig2.s);
74+
5375
// Verify signature via public key or address.
54-
// assert(sk.toPublicKey().verify(message, sig));
55-
// assert(sk.toPublicKey().toAddress().verify(message, sig));
76+
assert(pk.verify(m, sig));
77+
assert(addr.verify(m, sig));
5678

5779
// Default serialization (65 bytes).
5880
console.log("Default encoded signature:");
@@ -62,5 +84,6 @@ contract ECDSAExample is Script {
6284
// EIP-2098 serialization (64 bytes).
6385
console.log("EIP-2098 (compact) encoded signature:");
6486
console.logBytes(sig.toCompactEncoded());
87+
console.log("");
6588
}
6689
}

examples/secp256k1/signatures/Schnorr.sol

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import {
1414
import {SchnorrOffchain} from
1515
"src/offchain/secp256k1/signatures/SchnorrOffchain.sol";
1616
import {
17-
Schnorr, Signature
17+
Schnorr,
18+
Signature,
19+
SignatureCompressed
1820
} from "src/onchain/secp256k1/signatures/Schnorr.sol";
1921

2022
/**
@@ -32,28 +34,64 @@ contract SchnorrExample is Script {
3234
using Secp256k1 for SecretKey;
3335
using Secp256k1 for PublicKey;
3436

35-
using SchnorrOffchain for Signature;
3637
using SchnorrOffchain for SecretKey;
3738
using SchnorrOffchain for PublicKey;
39+
using SchnorrOffchain for Signature;
40+
using SchnorrOffchain for SignatureCompressed;
3841
using Schnorr for SecretKey;
3942
using Schnorr for PublicKey;
4043
using Schnorr for Signature;
44+
using Schnorr for SignatureCompressed;
4145

4246
function run() public {
43-
bytes memory message = bytes("crysol <3");
44-
45-
// Create a cryptographically secure secret key.
47+
// Create new cryptographically sound secret key and respective
48+
// public key.
4649
SecretKey sk = Secp256k1Offchain.newSecretKey();
47-
// assert(sk.isValid());
50+
PublicKey memory pk = sk.toPublicKey();
51+
52+
// Create digest of the message to sign.
53+
//
54+
// crysol's sign() functions only accept bytes32 digests to enforce
55+
// static payload size.
56+
bytes32 digest = keccak256(bytes("crysol <3"));
4857

49-
// Create Schnorr signature.
50-
Signature memory sig = sk.sign(message);
51-
// assert(!sig.isMalleable());
58+
// Note that crysol's sign() function domain separates input digests.
59+
// The actual message being signed can be constructed via:
60+
bytes32 m = Schnorr.constructMessageHash(digest);
61+
62+
// Sign digest via Schnorr.
63+
Signature memory sig = sk.sign(digest);
5264
console.log("Signed message via Schnorr, signature:");
5365
console.log(sig.toString());
5466
console.log("");
5567

56-
// Verify signature.
57-
// assert(sk.toPublicKey().verify(message, sig));
68+
// Note that Schnorr signatures can be compressed too.
69+
SignatureCompressed memory sigComp = sig.toCompressed();
70+
console.log("Compressed Schnorr signature:");
71+
console.log(sigComp.toString());
72+
console.log("");
73+
74+
// It's also possible to use the low-level signRaw() function to not
75+
// domain separate the input digest.
76+
// However, usage is discouraged.
77+
Signature memory sig2 = sk.signRaw(m);
78+
79+
// Note that crysol uses random nonces to construct Schnorr signatures.
80+
// Therefore, the two signatures are expected to not be equal.
81+
assert(sig.s != sig2.s);
82+
assert(!sig.r.eq(sig2.r));
83+
84+
// Verify signature via public key.
85+
assert(pk.verify(m, sig));
86+
87+
// Default serialization (96 bytes).
88+
console.log("Default encoded signature:");
89+
console.logBytes(sig.toEncoded());
90+
console.log("");
91+
92+
// Compressed serialization (52 bytes).
93+
console.log("Compressed Schnorr signature:");
94+
console.logBytes(sig.toCompressedEncoded());
95+
console.log("");
5896
}
5997
}

0 commit comments

Comments
 (0)