Skip to content

Commit

Permalink
schnorr: Adds Schnorr implementation following draft-ERC
Browse files Browse the repository at this point in the history
  • Loading branch information
pmerkleplant authored Sep 21, 2024
1 parent eff4232 commit 9548f31
Show file tree
Hide file tree
Showing 31 changed files with 1,598 additions and 958 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/solc-version-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
- name: Run forge tests against lowest and highest supported solc version
run: >
forge test --use 0.8.16 &&
forge test --use 0.8.26
forge test --use 0.8.27
18 changes: 15 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ clean: ## Clean build artifacts

.PHONY: test
test: ## Run full test suite
@forge test
@forge test --show-progress

.PHONY: test-intense
test-intense: ## Run full test suite with intense fuzzing
@FOUNDRY_PROFILE=intense forge test
@FOUNDRY_PROFILE=intense forge test --show-progress

.PHONY: test-summary
test-summary: ## Print summary of test suite
@forge test --summary
@forge test --summary --show-progress

.PHONY: coverage
coverage: ## Update coverage report and open lcov web interface
Expand Down Expand Up @@ -62,6 +62,18 @@ examples: ## Run examples
@echo "##"
@echo "########################################"
@forge script examples/secp256r1/Secp256r1.sol:Secp256r1Example -v
@echo "########################################"
@echo "##"
@echo "## ECDSA on secp56k1"
@echo "##"
@echo "########################################"
@forge script examples/secp256k1/signatures/ECDSA.sol:ECDSAExample -v
@echo "########################################"
@echo "##"
@echo "## Schnorr (ERC-XXX)"
@echo "##"
@echo "########################################"
@forge script examples/secp256k1/signatures/Schnorr.sol:SchnorrExample -v

.PHONY: fmt
fmt: ## Format project
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ $ forge install verklegarden/crysol
## Examples

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

Expand Down
27 changes: 27 additions & 0 deletions docs/Intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,30 @@ modifier vmed() {
// forgefmt: disable-end
// ~~~~~~~~~~~~~~~~~~~~~~~
```

## TODO: Signature Rules

- Malleable signatures are deemed invalid
- For ECDSA malleable signature if `s > Q/2` which is in sync with ecosystem
- Only 32 byte digests are signed, never byte strings
- It is always assumed that the digest is a keccak256 hash digest
- The default `sign` function MUST only sign domain separated messages
- For ECDSA using "Ethereum Signed Message" as defined via `eth_sign`
- For Schnorr its part of ERC-XXX
- There are `signRaw` functions that MUST NOT domain separate user input digests
- Usage is discouraged
- User input is called `digest`, which is domain separated to message `m`, wich is signed

- Terminology:
- A user wants to sign byte string `message`
- The `message` is hashed via keccak256 to `digest`
- The `digest` is domain separated to `m`

## TODO: Sanity Check Rules

- De/Serialization functions MUST revert if object is invalid/insane
- Example: It MUST NOT be possible to serialize an invalid public key
- Example: It MUST NOT be possible to deserialize a malleable ECDSA signature
- Type conversion functions MUST NOT revert if object is invalid/insane
- Example: It MUST be possible to transform an invalid public key to an point
- Example: It MUST be possible to transform an insance Schnorr signature to a compressed Schnorr signature
164 changes: 0 additions & 164 deletions docs/secp256k1/signatures/Schnorr.md

This file was deleted.

39 changes: 31 additions & 8 deletions examples/secp256k1/signatures/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -38,21 +38,43 @@ contract ECDSAExample is Script {
using ECDSA for Signature;

function run() public {
bytes memory message = bytes("crysol <3");

// Create new cryptographically sound secret key.
// Create new cryptographically sound secret key and respective
// public key and address.
SecretKey sk = Secp256k1Offchain.newSecretKey();
// assert(sk.isValid());
PublicKey memory pk = sk.toPublicKey();
address addr = pk.toAddress();

// Create digest of the message to sign.
//
// crysol's sign() functions only accept bytes32 digests to enforce
// static payload size.
bytes32 digest = keccak256(bytes("crysol <3"));

// Note that crysol's sign() function domain separates input digests.
// The actual message being signed can be constructed via:
bytes32 m = ECDSA.constructMessageHash(digest);

// Sign message via ECDSA.
Signature memory sig = sk.sign(message);
// Sign digest via ECDSA.
Signature memory sig = sk.sign(digest);
console.log("Signed message via ECDSA, signature:");
console.log(sig.toString());
console.log("");

// It's also possible to use the low-level signRaw() function to not
// domain separate the input digest.
// However, usage is discouraged.
Signature memory sig2 = sk.signRaw(m);

// Note that crysol uses RFC-6979 to construct deterministic ECDSA
// nonces and thereby signatures. Therefore, the two signatures are
// expected to be equal.
assert(sig.v == sig2.v);
assert(sig.r == sig2.r);
assert(sig.s == sig2.s);

// Verify signature via public key or address.
// assert(sk.toPublicKey().verify(message, sig));
// assert(sk.toPublicKey().toAddress().verify(message, sig));
assert(pk.verify(m, sig));
assert(addr.verify(m, sig));

// Default serialization (65 bytes).
console.log("Default encoded signature:");
Expand All @@ -62,5 +84,6 @@ contract ECDSAExample is Script {
// EIP-2098 serialization (64 bytes).
console.log("EIP-2098 (compact) encoded signature:");
console.logBytes(sig.toCompactEncoded());
console.log("");
}
}
60 changes: 49 additions & 11 deletions examples/secp256k1/signatures/Schnorr.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ import {
import {SchnorrOffchain} from
"src/offchain/secp256k1/signatures/SchnorrOffchain.sol";
import {
Schnorr, Signature
Schnorr,
Signature,
SignatureCompressed
} from "src/onchain/secp256k1/signatures/Schnorr.sol";

/**
Expand All @@ -32,28 +34,64 @@ contract SchnorrExample is Script {
using Secp256k1 for SecretKey;
using Secp256k1 for PublicKey;

using SchnorrOffchain for Signature;
using SchnorrOffchain for SecretKey;
using SchnorrOffchain for PublicKey;
using SchnorrOffchain for Signature;
using SchnorrOffchain for SignatureCompressed;
using Schnorr for SecretKey;
using Schnorr for PublicKey;
using Schnorr for Signature;
using Schnorr for SignatureCompressed;

function run() public {
bytes memory message = bytes("crysol <3");

// Create a cryptographically secure secret key.
// Create new cryptographically sound secret key and respective
// public key.
SecretKey sk = Secp256k1Offchain.newSecretKey();
// assert(sk.isValid());
PublicKey memory pk = sk.toPublicKey();

// Create digest of the message to sign.
//
// crysol's sign() functions only accept bytes32 digests to enforce
// static payload size.
bytes32 digest = keccak256(bytes("crysol <3"));

// Create Schnorr signature.
Signature memory sig = sk.sign(message);
// assert(!sig.isMalleable());
// Note that crysol's sign() function domain separates input digests.
// The actual message being signed can be constructed via:
bytes32 m = Schnorr.constructMessageHash(digest);

// Sign digest via Schnorr.
Signature memory sig = sk.sign(digest);
console.log("Signed message via Schnorr, signature:");
console.log(sig.toString());
console.log("");

// Verify signature.
// assert(sk.toPublicKey().verify(message, sig));
// Note that Schnorr signatures can be compressed too.
SignatureCompressed memory sigComp = sig.toCompressed();
console.log("Compressed Schnorr signature:");
console.log(sigComp.toString());
console.log("");

// It's also possible to use the low-level signRaw() function to not
// domain separate the input digest.
// However, usage is discouraged.
Signature memory sig2 = sk.signRaw(m);

// Note that crysol uses random nonces to construct Schnorr signatures.
// Therefore, the two signatures are expected to not be equal.
assert(sig.s != sig2.s);
assert(!sig.r.eq(sig2.r));

// Verify signature via public key.
assert(pk.verify(m, sig));

// Default serialization (96 bytes).
console.log("Default encoded signature:");
console.logBytes(sig.toEncoded());
console.log("");

// Compressed serialization (52 bytes).
console.log("Compressed Schnorr signature:");
console.logBytes(sig.toCompressedEncoded());
console.log("");
}
}
Loading

0 comments on commit 9548f31

Please sign in to comment.