mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-11 21:49:26 +00:00
Compare commits
20 Commits
078d6e51e5
...
95c30720d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95c30720d2 | ||
|
|
ceede14f5c | ||
|
|
5e60ea9718 | ||
|
|
153f6f2f2f | ||
|
|
104c0d4492 | ||
|
|
7c8f13ab28 | ||
|
|
cb0deadf9a | ||
|
|
cb489f9cef | ||
|
|
cc662cb591 | ||
|
|
a8b8844e3f | ||
|
|
82b543ef75 | ||
|
|
72e80c1a3d | ||
|
|
b6edc94bcd | ||
|
|
cfce2b26e2 | ||
|
|
e87bbcda64 | ||
|
|
9f84adf8b3 | ||
|
|
3919cf55ae | ||
|
|
38dd8cb191 | ||
|
|
f2563d39cb | ||
|
|
15a9cbef40 |
5
.github/workflows/crypto-tests.yml
vendored
5
.github/workflows/crypto-tests.yml
vendored
@@ -36,5 +36,10 @@ jobs:
|
|||||||
-p schnorr-signatures \
|
-p schnorr-signatures \
|
||||||
-p dleq \
|
-p dleq \
|
||||||
-p dkg \
|
-p dkg \
|
||||||
|
-p dkg-recovery \
|
||||||
|
-p dkg-dealer \
|
||||||
|
-p dkg-promote \
|
||||||
|
-p dkg-musig \
|
||||||
|
-p dkg-pedpop \
|
||||||
-p modular-frost \
|
-p modular-frost \
|
||||||
-p frost-schnorrkel
|
-p frost-schnorrkel
|
||||||
|
|||||||
129
Cargo.lock
generated
129
Cargo.lock
generated
@@ -1063,7 +1063,7 @@ checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitcoin-serai"
|
name = "bitcoin-serai"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -1075,6 +1075,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"simple-request",
|
"simple-request",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
|
"subtle",
|
||||||
"thiserror 1.0.64",
|
"thiserror 1.0.64",
|
||||||
"tokio",
|
"tokio",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
@@ -1979,7 +1980,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dalek-ff-group"
|
name = "dalek-ff-group"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-bigint",
|
"crypto-bigint",
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
@@ -2211,18 +2212,77 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dkg"
|
name = "dkg"
|
||||||
version = "0.5.1"
|
version = "0.6.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
|
"ciphersuite",
|
||||||
|
"std-shims",
|
||||||
|
"thiserror 2.0.14",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dkg-dealer"
|
||||||
|
version = "0.6.0"
|
||||||
|
dependencies = [
|
||||||
|
"ciphersuite",
|
||||||
|
"dkg",
|
||||||
|
"rand_core",
|
||||||
|
"std-shims",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dkg-musig"
|
||||||
|
version = "0.6.0"
|
||||||
|
dependencies = [
|
||||||
|
"ciphersuite",
|
||||||
|
"dkg",
|
||||||
|
"dkg-recovery",
|
||||||
|
"multiexp",
|
||||||
|
"rand_core",
|
||||||
|
"std-shims",
|
||||||
|
"thiserror 2.0.14",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dkg-pedpop"
|
||||||
|
version = "0.6.0"
|
||||||
|
dependencies = [
|
||||||
"chacha20",
|
"chacha20",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
|
"dkg",
|
||||||
"dleq",
|
"dleq",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"multiexp",
|
"multiexp",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"schnorr-signatures",
|
"schnorr-signatures",
|
||||||
"std-shims",
|
"thiserror 2.0.14",
|
||||||
"thiserror 1.0.64",
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dkg-promote"
|
||||||
|
version = "0.6.1"
|
||||||
|
dependencies = [
|
||||||
|
"ciphersuite",
|
||||||
|
"dkg",
|
||||||
|
"dkg-recovery",
|
||||||
|
"dleq",
|
||||||
|
"flexible-transcript",
|
||||||
|
"rand_core",
|
||||||
|
"thiserror 2.0.14",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dkg-recovery"
|
||||||
|
version = "0.6.0"
|
||||||
|
dependencies = [
|
||||||
|
"ciphersuite",
|
||||||
|
"dkg",
|
||||||
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2241,7 +2301,7 @@ dependencies = [
|
|||||||
"multiexp",
|
"multiexp",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2638,7 +2698,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flexible-transcript"
|
name = "flexible-transcript"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"blake2",
|
"blake2",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
@@ -2856,7 +2916,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "frost-schnorrkel"
|
name = "frost-schnorrkel"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
@@ -4753,7 +4813,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "minimal-ed448"
|
name = "minimal-ed448"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-bigint",
|
"crypto-bigint",
|
||||||
"ff",
|
"ff",
|
||||||
@@ -4823,12 +4883,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "modular-frost"
|
name = "modular-frost"
|
||||||
version = "0.9.0"
|
version = "0.10.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"dkg",
|
"dkg",
|
||||||
|
"dkg-dealer",
|
||||||
|
"dkg-recovery",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"hex",
|
"hex",
|
||||||
"minimal-ed448",
|
"minimal-ed448",
|
||||||
@@ -4838,27 +4900,27 @@ dependencies = [
|
|||||||
"schnorr-signatures",
|
"schnorr-signatures",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-address"
|
name = "monero-address"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-io",
|
"monero-io",
|
||||||
"monero-primitives",
|
"monero-primitives",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-borromean"
|
name = "monero-borromean"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
@@ -4871,7 +4933,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-bulletproofs"
|
name = "monero-bulletproofs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
@@ -4879,14 +4941,14 @@ dependencies = [
|
|||||||
"monero-primitives",
|
"monero-primitives",
|
||||||
"rand_core",
|
"rand_core",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-clsag"
|
name = "monero-clsag"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
@@ -4900,14 +4962,14 @@ dependencies = [
|
|||||||
"rand_core",
|
"rand_core",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"subtle",
|
"subtle",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-generators"
|
name = "monero-generators"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
@@ -4921,7 +4983,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-io"
|
name = "monero-io"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
@@ -4930,21 +4992,21 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-mlsag"
|
name = "monero-mlsag"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
"monero-io",
|
"monero-io",
|
||||||
"monero-primitives",
|
"monero-primitives",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-oxide"
|
name = "monero-oxide"
|
||||||
version = "0.1.4-alpha"
|
version = "0.1.4-alpha"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"hex-literal",
|
"hex-literal",
|
||||||
@@ -4962,7 +5024,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-primitives"
|
name = "monero-primitives"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"monero-generators",
|
"monero-generators",
|
||||||
@@ -4975,7 +5037,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-rpc"
|
name = "monero-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -4984,14 +5046,14 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-simple-request-rpc"
|
name = "monero-simple-request-rpc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest_auth",
|
"digest_auth",
|
||||||
"hex",
|
"hex",
|
||||||
@@ -5004,7 +5066,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "monero-wallet"
|
name = "monero-wallet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/monero-oxide/monero-oxide?rev=f19b0f57fe7cbbd643b51091c63de29afb0976e4#f19b0f57fe7cbbd643b51091c63de29afb0976e4"
|
source = "git+https://github.com/monero-oxide/monero-oxide?rev=a74f41c2270707e340a9cb57fcd97a762d04975b#a74f41c2270707e340a9cb57fcd97a762d04975b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
@@ -5021,7 +5083,7 @@ dependencies = [
|
|||||||
"rand_core",
|
"rand_core",
|
||||||
"rand_distr",
|
"rand_distr",
|
||||||
"std-shims",
|
"std-shims",
|
||||||
"thiserror 1.0.64",
|
"thiserror 2.0.14",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7988,6 +8050,7 @@ dependencies = [
|
|||||||
"bitcoin",
|
"bitcoin",
|
||||||
"blake2",
|
"blake2",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
|
"dkg-musig",
|
||||||
"dockertest",
|
"dockertest",
|
||||||
"frame-system",
|
"frame-system",
|
||||||
"frost-schnorrkel",
|
"frost-schnorrkel",
|
||||||
@@ -8047,6 +8110,7 @@ dependencies = [
|
|||||||
"blake2",
|
"blake2",
|
||||||
"borsh",
|
"borsh",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
|
"dkg-musig",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"frost-schnorrkel",
|
"frost-schnorrkel",
|
||||||
@@ -8324,6 +8388,9 @@ dependencies = [
|
|||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
"dkg",
|
"dkg",
|
||||||
|
"dkg-dealer",
|
||||||
|
"dkg-musig",
|
||||||
|
"dkg-recovery",
|
||||||
"dleq",
|
"dleq",
|
||||||
"flexible-transcript",
|
"flexible-transcript",
|
||||||
"minimal-ed448",
|
"minimal-ed448",
|
||||||
@@ -8415,10 +8482,12 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bitcoin-serai",
|
"bitcoin-serai",
|
||||||
|
"blake2",
|
||||||
"borsh",
|
"borsh",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"const-hex",
|
"const-hex",
|
||||||
"dalek-ff-group",
|
"dalek-ff-group",
|
||||||
|
"dkg-pedpop",
|
||||||
"dockertest",
|
"dockertest",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"ethereum-serai",
|
"ethereum-serai",
|
||||||
@@ -8603,7 +8672,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"borsh",
|
"borsh",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
"dkg",
|
"dkg-musig",
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
"scale-info",
|
"scale-info",
|
||||||
"serai-primitives",
|
"serai-primitives",
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ members = [
|
|||||||
"crypto/schnorr",
|
"crypto/schnorr",
|
||||||
"crypto/dleq",
|
"crypto/dleq",
|
||||||
"crypto/dkg",
|
"crypto/dkg",
|
||||||
|
"crypto/dkg/recovery",
|
||||||
|
"crypto/dkg/dealer",
|
||||||
|
"crypto/dkg/promote",
|
||||||
|
"crypto/dkg/musig",
|
||||||
|
"crypto/dkg/pedpop",
|
||||||
"crypto/frost",
|
"crypto/frost",
|
||||||
"crypto/schnorrkel",
|
"crypto/schnorrkel",
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/common/simple-requ
|
|||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["http", "https", "async", "request", "ssl"]
|
keywords = ["http", "https", "async", "request", "ssl"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.64"
|
rust-version = "1.70"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/common/zalloc"
|
|||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = []
|
keywords = []
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.77.0"
|
rust-version = "1.77"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ blake2 = { version = "0.10", default-features = false, features = ["std"] }
|
|||||||
transcript = { package = "flexible-transcript", path = "../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
transcript = { package = "flexible-transcript", path = "../crypto/transcript", default-features = false, features = ["std", "recommended"] }
|
||||||
ciphersuite = { path = "../crypto/ciphersuite", default-features = false, features = ["std"] }
|
ciphersuite = { path = "../crypto/ciphersuite", default-features = false, features = ["std"] }
|
||||||
schnorr = { package = "schnorr-signatures", path = "../crypto/schnorr", default-features = false, features = ["std"] }
|
schnorr = { package = "schnorr-signatures", path = "../crypto/schnorr", default-features = false, features = ["std"] }
|
||||||
|
dkg-musig = { path = "../crypto/dkg/musig", default-features = false, features = ["std"] }
|
||||||
frost = { package = "modular-frost", path = "../crypto/frost" }
|
frost = { package = "modular-frost", path = "../crypto/frost" }
|
||||||
frost-schnorrkel = { path = "../crypto/schnorrkel" }
|
frost-schnorrkel = { path = "../crypto/schnorrkel" }
|
||||||
|
|
||||||
|
|||||||
@@ -361,8 +361,8 @@ async fn dkg_test() {
|
|||||||
assert!(signature.verify(
|
assert!(signature.verify(
|
||||||
&*serai_client::validator_sets::primitives::set_keys_message(&set, &[], &key_pair),
|
&*serai_client::validator_sets::primitives::set_keys_message(&set, &[], &key_pair),
|
||||||
&serai_client::Public(
|
&serai_client::Public(
|
||||||
frost::dkg::musig::musig_key::<Ristretto>(
|
dkg_musig::musig_key_vartime::<Ristretto>(
|
||||||
&serai_client::validator_sets::primitives::musig_context(set.into()),
|
serai_client::validator_sets::primitives::musig_context(set.into()),
|
||||||
&self.spec.validators().into_iter().map(|(validator, _)| validator).collect::<Vec<_>>()
|
&self.spec.validators().into_iter().map(|(validator, _)| validator).collect::<Vec<_>>()
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
|||||||
@@ -67,12 +67,8 @@ use ciphersuite::{
|
|||||||
group::{ff::PrimeField, GroupEncoding},
|
group::{ff::PrimeField, GroupEncoding},
|
||||||
Ciphersuite, Ristretto,
|
Ciphersuite, Ristretto,
|
||||||
};
|
};
|
||||||
use frost::{
|
use dkg_musig::musig;
|
||||||
FrostError,
|
use frost::{FrostError, dkg::Participant, ThresholdKeys, sign::*};
|
||||||
dkg::{Participant, musig::musig},
|
|
||||||
ThresholdKeys,
|
|
||||||
sign::*,
|
|
||||||
};
|
|
||||||
use frost_schnorrkel::Schnorrkel;
|
use frost_schnorrkel::Schnorrkel;
|
||||||
|
|
||||||
use scale::Encode;
|
use scale::Encode;
|
||||||
@@ -119,7 +115,7 @@ impl<T: DbTxn, C: Encode> SigningProtocol<'_, T, C> {
|
|||||||
|
|
||||||
let algorithm = Schnorrkel::new(b"substrate");
|
let algorithm = Schnorrkel::new(b"substrate");
|
||||||
let keys: ThresholdKeys<Ristretto> =
|
let keys: ThresholdKeys<Ristretto> =
|
||||||
musig(&musig_context(self.spec.set().into()), self.key, participants)
|
musig(musig_context(self.spec.set().into()), self.key.clone(), participants)
|
||||||
.expect("signing for a set we aren't in/validator present multiple times")
|
.expect("signing for a set we aren't in/validator present multiple times")
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ciphersuite
|
|||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["ciphersuite", "ff", "group"]
|
keywords = ["ciphersuite", "ff", "group"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.74"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ macro_rules! dalek_curve {
|
|||||||
$Point::generator()
|
$Point::generator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reduce_512(mut scalar: [u8; 64]) -> Self::F {
|
||||||
|
let res = Scalar::from_bytes_mod_order_wide(&scalar);
|
||||||
|
scalar.zeroize();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||||
Scalar::from_hash(Sha512::new_with_prefix(&[dst, data].concat()))
|
Scalar::from_hash(Sha512::new_with_prefix(&[dst, data].concat()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ impl Ciphersuite for Ed448 {
|
|||||||
Point::generator()
|
Point::generator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reduce_512(mut scalar: [u8; 64]) -> Self::F {
|
||||||
|
let res = Self::hash_to_F(b"Ciphersuite-reduce_512", &scalar);
|
||||||
|
scalar.zeroize();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||||
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_ref().try_into().unwrap())
|
Scalar::wide_reduce(Self::H::digest([dst, data].concat()).as_ref().try_into().unwrap())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use group::ff::PrimeField;
|
|||||||
|
|
||||||
use elliptic_curve::{
|
use elliptic_curve::{
|
||||||
generic_array::GenericArray,
|
generic_array::GenericArray,
|
||||||
bigint::{NonZero, CheckedAdd, Encoding, U384},
|
bigint::{NonZero, CheckedAdd, Encoding, U384, U512},
|
||||||
hash2curve::{Expander, ExpandMsg, ExpandMsgXmd},
|
hash2curve::{Expander, ExpandMsg, ExpandMsgXmd},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,6 +31,22 @@ macro_rules! kp_curve {
|
|||||||
$lib::ProjectivePoint::GENERATOR
|
$lib::ProjectivePoint::GENERATOR
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||||
|
let mut modulus = [0; 64];
|
||||||
|
modulus[32 ..].copy_from_slice(&(Self::F::ZERO - Self::F::ONE).to_bytes());
|
||||||
|
let modulus = U512::from_be_slice(&modulus).checked_add(&U512::ONE).unwrap();
|
||||||
|
|
||||||
|
let mut wide =
|
||||||
|
U512::from_be_bytes(scalar).rem(&NonZero::new(modulus).unwrap()).to_be_bytes();
|
||||||
|
|
||||||
|
let mut array = *GenericArray::from_slice(&wide[32 ..]);
|
||||||
|
let res = $lib::Scalar::from_repr(array).unwrap();
|
||||||
|
|
||||||
|
wide.zeroize();
|
||||||
|
array.zeroize();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
|
fn hash_to_F(dst: &[u8], msg: &[u8]) -> Self::F {
|
||||||
// While one of these two libraries does support directly hashing to the Scalar field, the
|
// While one of these two libraries does support directly hashing to the Scalar field, the
|
||||||
// other doesn't. While that's probably an oversight, this is a universally working method
|
// other doesn't. While that's probably an oversight, this is a universally working method
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ pub trait Ciphersuite:
|
|||||||
// While group does provide this in its API, privacy coins may want to use a custom basepoint
|
// While group does provide this in its API, privacy coins may want to use a custom basepoint
|
||||||
fn generator() -> Self::G;
|
fn generator() -> Self::G;
|
||||||
|
|
||||||
|
/// Reduce 512 bits into a uniform scalar.
|
||||||
|
///
|
||||||
|
/// If 512 bits is insufficient to perform a reduction into a uniform scalar, the ciphersuite
|
||||||
|
/// will perform a hash to sample the necessary bits.
|
||||||
|
fn reduce_512(scalar: [u8; 64]) -> Self::F;
|
||||||
|
|
||||||
/// Hash the provided domain-separation tag and message to a scalar. Ciphersuites MAY naively
|
/// Hash the provided domain-separation tag and message to a scalar. Ciphersuites MAY naively
|
||||||
/// prefix the tag to the message, enabling transpotion between the two. Accordingly, this
|
/// prefix the tag to the message, enabling transpotion between the two. Accordingly, this
|
||||||
/// function should NOT be used in any scheme where one tag is a valid substring of another
|
/// function should NOT be used in any scheme where one tag is a valid substring of another
|
||||||
@@ -99,6 +105,9 @@ pub trait Ciphersuite:
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read a canonical point from something implementing std::io::Read.
|
/// Read a canonical point from something implementing std::io::Read.
|
||||||
|
///
|
||||||
|
/// The provided implementation is safe so long as `GroupEncoding::to_bytes` always returns a
|
||||||
|
/// canonical serialization.
|
||||||
#[cfg(any(feature = "alloc", feature = "std"))]
|
#[cfg(any(feature = "alloc", feature = "std"))]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> {
|
fn read_G<R: Read>(reader: &mut R) -> io::Result<Self::G> {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dalek-ff-group"
|
name = "dalek-ff-group"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
description = "ff/group bindings around curve25519-dalek"
|
description = "ff/group bindings around curve25519-dalek"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-group"
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-group"
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"]
|
keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.66"
|
rust-version = "1.65"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ impl_modulus!(
|
|||||||
type ResidueType = Residue<FieldModulus, { FieldModulus::LIMBS }>;
|
type ResidueType = Residue<FieldModulus, { FieldModulus::LIMBS }>;
|
||||||
|
|
||||||
/// A constant-time implementation of the Ed25519 field.
|
/// A constant-time implementation of the Ed25519 field.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, Zeroize)]
|
||||||
pub struct FieldElement(ResidueType);
|
pub struct FieldElement(ResidueType);
|
||||||
|
|
||||||
// Square root of -1.
|
// Square root of -1.
|
||||||
@@ -92,7 +92,7 @@ impl Neg for FieldElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Neg for &'a FieldElement {
|
impl Neg for &FieldElement {
|
||||||
type Output = FieldElement;
|
type Output = FieldElement;
|
||||||
fn neg(self) -> Self::Output {
|
fn neg(self) -> Self::Output {
|
||||||
(*self).neg()
|
(*self).neg()
|
||||||
|
|||||||
@@ -40,11 +40,19 @@ pub use field::FieldElement;
|
|||||||
|
|
||||||
// Use black_box when possible
|
// Use black_box when possible
|
||||||
#[rustversion::since(1.66)]
|
#[rustversion::since(1.66)]
|
||||||
use core::hint::black_box;
|
mod black_box {
|
||||||
#[rustversion::before(1.66)]
|
pub(crate) fn black_box<T>(val: T) -> T {
|
||||||
fn black_box<T>(val: T) -> T {
|
#[allow(clippy::incompatible_msrv)]
|
||||||
val
|
core::hint::black_box(val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#[rustversion::before(1.66)]
|
||||||
|
mod black_box {
|
||||||
|
pub(crate) fn black_box<T>(val: T) -> T {
|
||||||
|
val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use black_box::black_box;
|
||||||
|
|
||||||
fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
||||||
let bit_ref = black_box(bit_ref);
|
let bit_ref = black_box(bit_ref);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dkg"
|
name = "dkg"
|
||||||
version = "0.5.1"
|
version = "0.6.1"
|
||||||
description = "Distributed key generation over ff/group"
|
description = "Distributed key generation over ff/group"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg"
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg"
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.79"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -17,50 +17,28 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive", "alloc"] }
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
thiserror = { version = "2", default-features = false }
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
|
|
||||||
std-shims = { version = "0.1", path = "../../common/std-shims", default-features = false }
|
std-shims = { version = "0.1", path = "../../common/std-shims", default-features = false }
|
||||||
|
|
||||||
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
|
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../transcript", version = "^0.3.2", default-features = false, features = ["recommended"] }
|
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false, features = ["alloc"] }
|
||||||
chacha20 = { version = "0.9", default-features = false, features = ["zeroize"] }
|
|
||||||
|
|
||||||
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", default-features = false }
|
|
||||||
multiexp = { path = "../multiexp", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1", default-features = false }
|
|
||||||
dleq = { path = "../dleq", version = "^0.4.1", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
|
||||||
ciphersuite = { path = "../ciphersuite", default-features = false, features = ["ristretto"] }
|
ciphersuite = { path = "../ciphersuite", default-features = false, features = ["ristretto"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = [
|
std = [
|
||||||
"thiserror",
|
"thiserror/std",
|
||||||
|
|
||||||
"rand_core/std",
|
|
||||||
|
|
||||||
"std-shims/std",
|
"std-shims/std",
|
||||||
|
|
||||||
"borsh?/std",
|
"borsh?/std",
|
||||||
|
|
||||||
"transcript/std",
|
|
||||||
"chacha20/std",
|
|
||||||
|
|
||||||
"ciphersuite/std",
|
"ciphersuite/std",
|
||||||
"multiexp/std",
|
|
||||||
"multiexp/batch",
|
|
||||||
|
|
||||||
"schnorr/std",
|
|
||||||
"dleq/std",
|
|
||||||
"dleq/serialize"
|
|
||||||
]
|
]
|
||||||
borsh = ["dep:borsh"]
|
borsh = ["dep:borsh"]
|
||||||
tests = ["rand_core/getrandom"]
|
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021-2023 Luke Parker
|
Copyright (c) 2021-2025 Luke Parker
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
# Distributed Key Generation
|
# Distributed Key Generation
|
||||||
|
|
||||||
A collection of implementations of various distributed key generation protocols.
|
A crate implementing a type for keys, presumably the result of a distributed
|
||||||
|
key generation protocol, and utilities from there.
|
||||||
|
|
||||||
All included protocols resolve into the provided `Threshold` types, intended to
|
This crate used to host implementations of distributed key generation protocols
|
||||||
enable their modularity. Additional utilities around these types, such as
|
as well (hence the name). Those have been smashed into their own crates, such
|
||||||
promotion from one generator to another, are also provided.
|
as [`dkg-musig`](https://docs.rs/dkg-musig) and
|
||||||
|
[`dkg-pedpop`](https://docs.rs/dkg-pedpop).
|
||||||
|
|
||||||
Currently, the only included protocol is the two-round protocol from the
|
Before being smashed, this crate was [audited by Cypher Stack in March 2023](
|
||||||
[FROST paper](https://eprint.iacr.org/2020/852).
|
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||||
|
), culminating in commit [669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||||
This library was
|
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
). Any subsequent changes have not undergone auditing.
|
||||||
culminating in commit
|
|
||||||
[669d2dbffc1dafb82a09d9419ea182667115df06](https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06).
|
|
||||||
Any subsequent changes have not undergone auditing.
|
|
||||||
|
|||||||
36
crypto/dkg/dealer/Cargo.toml
Normal file
36
crypto/dkg/dealer/Cargo.toml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "dkg-dealer"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "Produce dkg::ThresholdKeys with a dealer key generation"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/dealer"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zeroize = { version = "^1.5", default-features = false }
|
||||||
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
|
std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false }
|
||||||
|
|
||||||
|
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||||
|
dkg = { path = "../", version = "0.6", default-features = false }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
std = [
|
||||||
|
"zeroize/std",
|
||||||
|
"rand_core/std",
|
||||||
|
"std-shims/std",
|
||||||
|
"ciphersuite/std",
|
||||||
|
"dkg/std",
|
||||||
|
]
|
||||||
|
default = ["std"]
|
||||||
21
crypto/dkg/dealer/LICENSE
Normal file
21
crypto/dkg/dealer/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-2025 Luke Parker
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
13
crypto/dkg/dealer/README.md
Normal file
13
crypto/dkg/dealer/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Distributed Key Generation - Dealer
|
||||||
|
|
||||||
|
This crate implements a dealer key generation protocol for the
|
||||||
|
[`dkg`](https://docs.rs/dkg) crate's types. This provides a single point of
|
||||||
|
failure when the key is being generated and is NOT recommended for use outside
|
||||||
|
of tests.
|
||||||
|
|
||||||
|
This crate was originally part of (in some form) the `dkg` crate, which was
|
||||||
|
[audited by Cypher Stack in March 2023](
|
||||||
|
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||||
|
), culminating in commit [669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||||
|
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||||
|
). Any subsequent changes have not undergone auditing.
|
||||||
68
crypto/dkg/dealer/src/lib.rs
Normal file
68
crypto/dkg/dealer/src/lib.rs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use core::ops::Deref;
|
||||||
|
use std_shims::{vec::Vec, collections::HashMap};
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use ciphersuite::{
|
||||||
|
group::ff::{Field, PrimeField},
|
||||||
|
Ciphersuite,
|
||||||
|
};
|
||||||
|
pub use dkg::*;
|
||||||
|
|
||||||
|
/// Create a key via a dealer key generation protocol.
|
||||||
|
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
|
rng: &mut R,
|
||||||
|
threshold: u16,
|
||||||
|
participants: u16,
|
||||||
|
) -> Result<HashMap<Participant, ThresholdKeys<C>>, DkgError> {
|
||||||
|
let mut coefficients = Vec::with_capacity(usize::from(participants));
|
||||||
|
// `.max(1)` so we always generate the 0th coefficient which we'll share
|
||||||
|
for _ in 0 .. threshold.max(1) {
|
||||||
|
coefficients.push(Zeroizing::new(C::F::random(&mut *rng)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn polynomial<F: PrimeField + Zeroize>(
|
||||||
|
coefficients: &[Zeroizing<F>],
|
||||||
|
l: Participant,
|
||||||
|
) -> Zeroizing<F> {
|
||||||
|
let l = F::from(u64::from(u16::from(l)));
|
||||||
|
// This should never be reached since Participant is explicitly non-zero
|
||||||
|
assert!(l != F::ZERO, "zero participant passed to polynomial");
|
||||||
|
let mut share = Zeroizing::new(F::ZERO);
|
||||||
|
for (idx, coefficient) in coefficients.iter().rev().enumerate() {
|
||||||
|
*share += coefficient.deref();
|
||||||
|
if idx != (coefficients.len() - 1) {
|
||||||
|
*share *= l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
share
|
||||||
|
}
|
||||||
|
|
||||||
|
let group_key = C::generator() * coefficients[0].deref();
|
||||||
|
let mut secret_shares = HashMap::with_capacity(participants as usize);
|
||||||
|
let mut verification_shares = HashMap::with_capacity(participants as usize);
|
||||||
|
for i in 1 ..= participants {
|
||||||
|
let i = Participant::new(i).expect("non-zero u16 wasn't a valid Participant index");
|
||||||
|
let secret_share = polynomial(&coefficients, i);
|
||||||
|
secret_shares.insert(i, secret_share.clone());
|
||||||
|
verification_shares.insert(i, C::generator() * *secret_share);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res = HashMap::with_capacity(participants as usize);
|
||||||
|
for (i, secret_share) in secret_shares {
|
||||||
|
let keys = ThresholdKeys::new(
|
||||||
|
ThresholdParams::new(threshold, participants, i)?,
|
||||||
|
Interpolation::Lagrange,
|
||||||
|
secret_share,
|
||||||
|
verification_shares.clone(),
|
||||||
|
)?;
|
||||||
|
debug_assert_eq!(keys.group_key(), group_key);
|
||||||
|
res.insert(i, keys);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
49
crypto/dkg/musig/Cargo.toml
Normal file
49
crypto/dkg/musig/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
[package]
|
||||||
|
name = "dkg-musig"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "The MuSig key aggregation protocol"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/musig"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = { version = "2", default-features = false }
|
||||||
|
|
||||||
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
|
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||||
|
|
||||||
|
std-shims = { version = "0.1", path = "../../../common/std-shims", default-features = false }
|
||||||
|
|
||||||
|
multiexp = { path = "../../multiexp", version = "0.4", default-features = false }
|
||||||
|
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||||
|
dkg = { path = "../", version = "0.6", default-features = false }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||||
|
ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["ristretto"] }
|
||||||
|
dkg-recovery = { path = "../recovery", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
std = [
|
||||||
|
"thiserror/std",
|
||||||
|
|
||||||
|
"rand_core/std",
|
||||||
|
|
||||||
|
"std-shims/std",
|
||||||
|
|
||||||
|
"multiexp/std",
|
||||||
|
"ciphersuite/std",
|
||||||
|
"dkg/std",
|
||||||
|
]
|
||||||
|
default = ["std"]
|
||||||
21
crypto/dkg/musig/LICENSE
Normal file
21
crypto/dkg/musig/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-2025 Luke Parker
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
12
crypto/dkg/musig/README.md
Normal file
12
crypto/dkg/musig/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Distributed Key Generation - MuSig
|
||||||
|
|
||||||
|
This implements the MuSig key aggregation protocol for the
|
||||||
|
[`dkg`](https://docs.rs/dkg) crate's types.
|
||||||
|
|
||||||
|
This crate was originally part of (in some form) the `dkg` crate, which was
|
||||||
|
[audited by Cypher Stack in March 2023](
|
||||||
|
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||||
|
), culminating in commit
|
||||||
|
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||||
|
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||||
|
). Any subsequent changes have not undergone auditing.
|
||||||
162
crypto/dkg/musig/src/lib.rs
Normal file
162
crypto/dkg/musig/src/lib.rs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
|
use core::ops::Deref;
|
||||||
|
use std_shims::{
|
||||||
|
vec,
|
||||||
|
vec::Vec,
|
||||||
|
collections::{HashSet, HashMap},
|
||||||
|
};
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
||||||
|
|
||||||
|
pub use dkg::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
/// Errors encountered when working with threshold keys.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||||
|
pub enum MusigError<C: Ciphersuite> {
|
||||||
|
/// No keys were provided.
|
||||||
|
#[error("no keys provided")]
|
||||||
|
NoKeysProvided,
|
||||||
|
/// Too many keys were provided.
|
||||||
|
#[error("too many keys (allowed {max}, provided {provided})")]
|
||||||
|
TooManyKeysProvided {
|
||||||
|
/// The maximum amount of keys allowed.
|
||||||
|
max: u16,
|
||||||
|
/// The amount of keys provided.
|
||||||
|
provided: usize,
|
||||||
|
},
|
||||||
|
/// A participant was duplicated.
|
||||||
|
#[error("a participant was duplicated")]
|
||||||
|
DuplicatedParticipant(C::G),
|
||||||
|
/// Participating, yet our public key wasn't found in the list of keys.
|
||||||
|
#[error("private key's public key wasn't present in the list of public keys")]
|
||||||
|
NotPresent,
|
||||||
|
/// An error propagated from the underlying `dkg` crate.
|
||||||
|
#[error("error from dkg ({0})")]
|
||||||
|
DkgError(DkgError),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, MusigError<C>> {
|
||||||
|
if keys.is_empty() {
|
||||||
|
Err(MusigError::NoKeysProvided)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let keys_len = u16::try_from(keys.len())
|
||||||
|
.map_err(|_| MusigError::TooManyKeysProvided { max: u16::MAX, provided: keys.len() })?;
|
||||||
|
|
||||||
|
let mut set = HashSet::with_capacity(keys.len());
|
||||||
|
for key in keys {
|
||||||
|
let bytes = key.to_bytes().as_ref().to_vec();
|
||||||
|
if !set.insert(bytes) {
|
||||||
|
Err(MusigError::DuplicatedParticipant(*key))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(keys_len)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binding_factor_transcript<C: Ciphersuite>(
|
||||||
|
context: [u8; 32],
|
||||||
|
keys_len: u16,
|
||||||
|
keys: &[C::G],
|
||||||
|
) -> Vec<u8> {
|
||||||
|
debug_assert_eq!(usize::from(keys_len), keys.len());
|
||||||
|
|
||||||
|
let mut transcript = vec![];
|
||||||
|
transcript.extend(&context);
|
||||||
|
transcript.extend(keys_len.to_le_bytes());
|
||||||
|
for key in keys {
|
||||||
|
transcript.extend(key.to_bytes().as_ref());
|
||||||
|
}
|
||||||
|
transcript
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binding_factor<C: Ciphersuite>(mut transcript: Vec<u8>, i: u16) -> C::F {
|
||||||
|
transcript.extend(i.to_le_bytes());
|
||||||
|
C::hash_to_F(b"dkg-musig", &transcript)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn musig_key_multiexp<C: Ciphersuite>(
|
||||||
|
context: [u8; 32],
|
||||||
|
keys: &[C::G],
|
||||||
|
) -> Result<Vec<(C::F, C::G)>, MusigError<C>> {
|
||||||
|
let keys_len = check_keys::<C>(keys)?;
|
||||||
|
let transcript = binding_factor_transcript::<C>(context, keys_len, keys);
|
||||||
|
let mut multiexp = Vec::with_capacity(keys.len());
|
||||||
|
for i in 1 ..= keys_len {
|
||||||
|
multiexp.push((binding_factor::<C>(transcript.clone(), i), keys[usize::from(i - 1)]));
|
||||||
|
}
|
||||||
|
Ok(multiexp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The group key resulting from using this library's MuSig key aggregation.
|
||||||
|
///
|
||||||
|
/// This function executes in variable time and MUST NOT be used with secret data.
|
||||||
|
pub fn musig_key_vartime<C: Ciphersuite>(
|
||||||
|
context: [u8; 32],
|
||||||
|
keys: &[C::G],
|
||||||
|
) -> Result<C::G, MusigError<C>> {
|
||||||
|
Ok(multiexp::multiexp_vartime(&musig_key_multiexp(context, keys)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The group key resulting from using this library's MuSig key aggregation.
|
||||||
|
pub fn musig_key<C: Ciphersuite>(context: [u8; 32], keys: &[C::G]) -> Result<C::G, MusigError<C>> {
|
||||||
|
Ok(multiexp::multiexp(&musig_key_multiexp(context, keys)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
||||||
|
pub fn musig<C: Ciphersuite>(
|
||||||
|
context: [u8; 32],
|
||||||
|
private_key: Zeroizing<C::F>,
|
||||||
|
keys: &[C::G],
|
||||||
|
) -> Result<ThresholdKeys<C>, MusigError<C>> {
|
||||||
|
let our_pub_key = C::generator() * private_key.deref();
|
||||||
|
let Some(our_i) = keys.iter().position(|key| *key == our_pub_key) else {
|
||||||
|
Err(MusigError::DkgError(DkgError::NotParticipating))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let keys_len: u16 = check_keys::<C>(keys)?;
|
||||||
|
|
||||||
|
let params = ThresholdParams::new(
|
||||||
|
keys_len,
|
||||||
|
keys_len,
|
||||||
|
// The `+ 1` won't fail as `keys.len() <= u16::MAX`, so any index is `< u16::MAX`
|
||||||
|
Participant::new(
|
||||||
|
u16::try_from(our_i).expect("keys.len() <= u16::MAX yet index of keys > u16::MAX?") + 1,
|
||||||
|
)
|
||||||
|
.expect("i + 1 != 0"),
|
||||||
|
)
|
||||||
|
.map_err(MusigError::DkgError)?;
|
||||||
|
|
||||||
|
let transcript = binding_factor_transcript::<C>(context, keys_len, keys);
|
||||||
|
let mut binding_factors = Vec::with_capacity(keys.len());
|
||||||
|
let mut multiexp = Vec::with_capacity(keys.len());
|
||||||
|
let mut verification_shares = HashMap::with_capacity(keys.len());
|
||||||
|
for (i, key) in (1 ..= keys_len).zip(keys.iter().copied()) {
|
||||||
|
let binding_factor = binding_factor::<C>(transcript.clone(), i);
|
||||||
|
binding_factors.push(binding_factor);
|
||||||
|
multiexp.push((binding_factor, key));
|
||||||
|
|
||||||
|
let i = Participant::new(i).expect("non-zero u16 wasn't a valid Participant index?");
|
||||||
|
verification_shares.insert(i, key);
|
||||||
|
}
|
||||||
|
let group_key = multiexp::multiexp(&multiexp);
|
||||||
|
debug_assert_eq!(our_pub_key, verification_shares[¶ms.i()]);
|
||||||
|
debug_assert_eq!(musig_key_vartime::<C>(context, keys).unwrap(), group_key);
|
||||||
|
|
||||||
|
ThresholdKeys::new(
|
||||||
|
params,
|
||||||
|
Interpolation::Constant(binding_factors),
|
||||||
|
private_key,
|
||||||
|
verification_shares,
|
||||||
|
)
|
||||||
|
.map_err(MusigError::DkgError)
|
||||||
|
}
|
||||||
70
crypto/dkg/musig/src/tests.rs
Normal file
70
crypto/dkg/musig/src/tests.rs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use ciphersuite::{group::ff::Field, Ciphersuite, Ristretto};
|
||||||
|
|
||||||
|
use dkg_recovery::recover_key;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
/// Tests MuSig key generation.
|
||||||
|
#[test]
|
||||||
|
pub fn test_musig() {
|
||||||
|
const PARTICIPANTS: u16 = 5;
|
||||||
|
|
||||||
|
let mut keys = vec![];
|
||||||
|
let mut pub_keys = vec![];
|
||||||
|
for _ in 0 .. PARTICIPANTS {
|
||||||
|
let key = Zeroizing::new(<Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||||
|
pub_keys.push(<Ristretto as Ciphersuite>::generator() * *key);
|
||||||
|
keys.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONTEXT: [u8; 32] = *b"MuSig Test ";
|
||||||
|
|
||||||
|
// Empty signing set
|
||||||
|
musig::<Ristretto>(CONTEXT, Zeroizing::new(<Ristretto as Ciphersuite>::F::ZERO), &[])
|
||||||
|
.unwrap_err();
|
||||||
|
// Signing set we're not part of
|
||||||
|
musig::<Ristretto>(
|
||||||
|
CONTEXT,
|
||||||
|
Zeroizing::new(<Ristretto as Ciphersuite>::F::ZERO),
|
||||||
|
&[<Ristretto as Ciphersuite>::generator()],
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
// Test with n keys
|
||||||
|
{
|
||||||
|
let mut created_keys = HashMap::new();
|
||||||
|
let mut verification_shares = HashMap::new();
|
||||||
|
let group_key = musig_key::<Ristretto>(CONTEXT, &pub_keys).unwrap();
|
||||||
|
for (i, key) in keys.iter().enumerate() {
|
||||||
|
let these_keys = musig::<Ristretto>(CONTEXT, key.clone(), &pub_keys).unwrap();
|
||||||
|
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
||||||
|
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
||||||
|
assert_eq!(usize::from(u16::from(these_keys.params().i())), i + 1);
|
||||||
|
|
||||||
|
verification_shares.insert(
|
||||||
|
these_keys.params().i(),
|
||||||
|
<Ristretto as Ciphersuite>::generator() * **these_keys.original_secret_share(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(these_keys.group_key(), group_key);
|
||||||
|
|
||||||
|
created_keys.insert(these_keys.params().i(), these_keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
for keys in created_keys.values() {
|
||||||
|
for (l, verification_share) in &verification_shares {
|
||||||
|
assert_eq!(keys.original_verification_share(*l), *verification_share);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
<Ristretto as Ciphersuite>::generator() *
|
||||||
|
*recover_key(&created_keys.values().cloned().collect::<Vec<_>>()).unwrap(),
|
||||||
|
group_key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
crypto/dkg/pedpop/Cargo.toml
Normal file
37
crypto/dkg/pedpop/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "dkg-pedpop"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "The PedPoP distributed key generation protocol"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/pedpop"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = { version = "2", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.3", default-features = false, features = ["std", "recommended"] }
|
||||||
|
chacha20 = { version = "0.9", default-features = false, features = ["std", "zeroize"] }
|
||||||
|
|
||||||
|
multiexp = { path = "../../multiexp", version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
||||||
|
schnorr = { package = "schnorr-signatures", path = "../../schnorr", version = "^0.5.1", default-features = false, features = ["std"] }
|
||||||
|
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
||||||
|
|
||||||
|
dkg = { path = "../", version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||||
|
ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["ristretto"] }
|
||||||
21
crypto/dkg/pedpop/LICENSE
Normal file
21
crypto/dkg/pedpop/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-2025 Luke Parker
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
12
crypto/dkg/pedpop/README.md
Normal file
12
crypto/dkg/pedpop/README.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Distributed Key Generation - PedPoP
|
||||||
|
|
||||||
|
This implements the PedPoP distributed key generation protocol for the
|
||||||
|
[`dkg`](https://docs.rs/dkg) crate's types.
|
||||||
|
|
||||||
|
This crate was originally part of the `dkg` crate, which was
|
||||||
|
[audited by Cypher Stack in March 2023](
|
||||||
|
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||||
|
), culminating in commit
|
||||||
|
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||||
|
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||||
|
). Any subsequent changes have not undergone auditing.
|
||||||
@@ -21,7 +21,7 @@ use multiexp::BatchVerifier;
|
|||||||
use schnorr::SchnorrSignature;
|
use schnorr::SchnorrSignature;
|
||||||
use dleq::DLEqProof;
|
use dleq::DLEqProof;
|
||||||
|
|
||||||
use crate::{Participant, ThresholdParams};
|
use dkg::{Participant, ThresholdParams};
|
||||||
|
|
||||||
mod sealed {
|
mod sealed {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -69,7 +69,7 @@ impl<C: Ciphersuite, M: Message> EncryptionKeyMessage<C, M> {
|
|||||||
buf
|
buf
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "tests"))]
|
#[cfg(test)]
|
||||||
pub(crate) fn enc_key(&self) -> C::G {
|
pub(crate) fn enc_key(&self) -> C::G {
|
||||||
self.enc_key
|
self.enc_key
|
||||||
}
|
}
|
||||||
@@ -98,11 +98,11 @@ fn ecdh<C: Ciphersuite>(private: &Zeroizing<C::F>, public: C::G) -> Zeroizing<C:
|
|||||||
|
|
||||||
// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be
|
// Each ecdh must be distinct. Reuse of an ecdh for multiple ciphers will cause the messages to be
|
||||||
// leaked.
|
// leaked.
|
||||||
fn cipher<C: Ciphersuite>(context: &str, ecdh: &Zeroizing<C::G>) -> ChaCha20 {
|
fn cipher<C: Ciphersuite>(context: [u8; 32], ecdh: &Zeroizing<C::G>) -> ChaCha20 {
|
||||||
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
|
// Ideally, we'd box this transcript with ZAlloc, yet that's only possible on nightly
|
||||||
// TODO: https://github.com/serai-dex/serai/issues/151
|
// TODO: https://github.com/serai-dex/serai/issues/151
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
|
let mut transcript = RecommendedTranscript::new(b"DKG Encryption v0.2");
|
||||||
transcript.append_message(b"context", context.as_bytes());
|
transcript.append_message(b"context", context);
|
||||||
|
|
||||||
transcript.domain_separate(b"encryption_key");
|
transcript.domain_separate(b"encryption_key");
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ fn cipher<C: Ciphersuite>(context: &str, ecdh: &Zeroizing<C::G>) -> ChaCha20 {
|
|||||||
|
|
||||||
fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
|
fn encrypt<R: RngCore + CryptoRng, C: Ciphersuite, E: Encryptable>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
context: &str,
|
context: [u8; 32],
|
||||||
from: Participant,
|
from: Participant,
|
||||||
to: C::G,
|
to: C::G,
|
||||||
mut msg: Zeroizing<E>,
|
mut msg: Zeroizing<E>,
|
||||||
@@ -197,7 +197,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
|
|||||||
pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>(
|
pub(crate) fn invalidate_msg<R: RngCore + CryptoRng>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
context: &str,
|
context: [u8; 32],
|
||||||
from: Participant,
|
from: Participant,
|
||||||
) {
|
) {
|
||||||
// Invalidate the message by specifying a new key/Schnorr PoP
|
// Invalidate the message by specifying a new key/Schnorr PoP
|
||||||
@@ -219,7 +219,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
|
|||||||
pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>(
|
pub(crate) fn invalidate_share_serialization<R: RngCore + CryptoRng>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
context: &str,
|
context: [u8; 32],
|
||||||
from: Participant,
|
from: Participant,
|
||||||
to: C::G,
|
to: C::G,
|
||||||
) {
|
) {
|
||||||
@@ -243,7 +243,7 @@ impl<C: Ciphersuite, E: Encryptable> EncryptedMessage<C, E> {
|
|||||||
pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>(
|
pub(crate) fn invalidate_share_value<R: RngCore + CryptoRng>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
context: &str,
|
context: [u8; 32],
|
||||||
from: Participant,
|
from: Participant,
|
||||||
to: C::G,
|
to: C::G,
|
||||||
) {
|
) {
|
||||||
@@ -300,14 +300,14 @@ impl<C: Ciphersuite> EncryptionKeyProof<C> {
|
|||||||
// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no
|
// This still doesn't mean the DKG offers an authenticated channel. The per-message keys have no
|
||||||
// root of trust other than their existence in the assumed-to-exist external authenticated channel.
|
// root of trust other than their existence in the assumed-to-exist external authenticated channel.
|
||||||
fn pop_challenge<C: Ciphersuite>(
|
fn pop_challenge<C: Ciphersuite>(
|
||||||
context: &str,
|
context: [u8; 32],
|
||||||
nonce: C::G,
|
nonce: C::G,
|
||||||
key: C::G,
|
key: C::G,
|
||||||
sender: Participant,
|
sender: Participant,
|
||||||
msg: &[u8],
|
msg: &[u8],
|
||||||
) -> C::F {
|
) -> C::F {
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
|
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Proof of Possession v0.2");
|
||||||
transcript.append_message(b"context", context.as_bytes());
|
transcript.append_message(b"context", context);
|
||||||
|
|
||||||
transcript.domain_separate(b"proof_of_possession");
|
transcript.domain_separate(b"proof_of_possession");
|
||||||
|
|
||||||
@@ -323,9 +323,9 @@ fn pop_challenge<C: Ciphersuite>(
|
|||||||
C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr"))
|
C::hash_to_F(b"DKG-encryption-proof_of_possession", &transcript.challenge(b"schnorr"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encryption_key_transcript(context: &str) -> RecommendedTranscript {
|
fn encryption_key_transcript(context: [u8; 32]) -> RecommendedTranscript {
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2");
|
let mut transcript = RecommendedTranscript::new(b"DKG Encryption Key Correctness Proof v0.2");
|
||||||
transcript.append_message(b"context", context.as_bytes());
|
transcript.append_message(b"context", context);
|
||||||
transcript
|
transcript
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,58 +337,17 @@ pub(crate) enum DecryptionError {
|
|||||||
InvalidProof,
|
InvalidProof,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A simple box for managing encryption.
|
// A simple box for managing decryption.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct Encryption<C: Ciphersuite> {
|
pub(crate) struct Decryption<C: Ciphersuite> {
|
||||||
context: String,
|
context: [u8; 32],
|
||||||
i: Option<Participant>,
|
|
||||||
enc_key: Zeroizing<C::F>,
|
|
||||||
enc_pub_key: C::G,
|
|
||||||
enc_keys: HashMap<Participant, C::G>,
|
enc_keys: HashMap<Participant, C::G>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for Encryption<C> {
|
impl<C: Ciphersuite> Decryption<C> {
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
pub(crate) fn new(context: [u8; 32]) -> Self {
|
||||||
fmt
|
Self { context, enc_keys: HashMap::new() }
|
||||||
.debug_struct("Encryption")
|
|
||||||
.field("context", &self.context)
|
|
||||||
.field("i", &self.i)
|
|
||||||
.field("enc_pub_key", &self.enc_pub_key)
|
|
||||||
.field("enc_keys", &self.enc_keys)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for Encryption<C> {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.enc_key.zeroize();
|
|
||||||
self.enc_pub_key.zeroize();
|
|
||||||
for (_, mut value) in self.enc_keys.drain() {
|
|
||||||
value.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Encryption<C> {
|
|
||||||
pub(crate) fn new<R: RngCore + CryptoRng>(
|
|
||||||
context: String,
|
|
||||||
i: Option<Participant>,
|
|
||||||
rng: &mut R,
|
|
||||||
) -> Self {
|
|
||||||
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
|
||||||
Self {
|
|
||||||
context,
|
|
||||||
i,
|
|
||||||
enc_pub_key: C::generator() * enc_key.deref(),
|
|
||||||
enc_key,
|
|
||||||
enc_keys: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
|
||||||
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn register<M: Message>(
|
pub(crate) fn register<M: Message>(
|
||||||
&mut self,
|
&mut self,
|
||||||
participant: Participant,
|
participant: Participant,
|
||||||
@@ -402,13 +361,109 @@ impl<C: Ciphersuite> Encryption<C> {
|
|||||||
msg.msg
|
msg.msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a message, and the intended decryptor, and a proof for its key, decrypt the message.
|
||||||
|
// Returns None if the key was wrong.
|
||||||
|
pub(crate) fn decrypt_with_proof<E: Encryptable>(
|
||||||
|
&self,
|
||||||
|
from: Participant,
|
||||||
|
decryptor: Participant,
|
||||||
|
mut msg: EncryptedMessage<C, E>,
|
||||||
|
// There's no encryption key proof if the accusation is of an invalid signature
|
||||||
|
proof: Option<EncryptionKeyProof<C>>,
|
||||||
|
) -> Result<Zeroizing<E>, DecryptionError> {
|
||||||
|
if !msg.pop.verify(
|
||||||
|
msg.key,
|
||||||
|
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
||||||
|
) {
|
||||||
|
Err(DecryptionError::InvalidSignature)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(proof) = proof {
|
||||||
|
// Verify this is the decryption key for this message
|
||||||
|
proof
|
||||||
|
.dleq
|
||||||
|
.verify(
|
||||||
|
&mut encryption_key_transcript(self.context),
|
||||||
|
&[C::generator(), msg.key],
|
||||||
|
&[self.enc_keys[&decryptor], *proof.key],
|
||||||
|
)
|
||||||
|
.map_err(|_| DecryptionError::InvalidProof)?;
|
||||||
|
|
||||||
|
cipher::<C>(self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
|
||||||
|
Ok(msg.msg)
|
||||||
|
} else {
|
||||||
|
Err(DecryptionError::InvalidProof)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A simple box for managing encryption.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Encryption<C: Ciphersuite> {
|
||||||
|
context: [u8; 32],
|
||||||
|
i: Participant,
|
||||||
|
enc_key: Zeroizing<C::F>,
|
||||||
|
enc_pub_key: C::G,
|
||||||
|
decryption: Decryption<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> fmt::Debug for Encryption<C> {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt
|
||||||
|
.debug_struct("Encryption")
|
||||||
|
.field("context", &self.context)
|
||||||
|
.field("i", &self.i)
|
||||||
|
.field("enc_pub_key", &self.enc_pub_key)
|
||||||
|
.field("decryption", &self.decryption)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Zeroize for Encryption<C> {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.enc_key.zeroize();
|
||||||
|
self.enc_pub_key.zeroize();
|
||||||
|
for (_, mut value) in self.decryption.enc_keys.drain() {
|
||||||
|
value.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Encryption<C> {
|
||||||
|
pub(crate) fn new<R: RngCore + CryptoRng>(
|
||||||
|
context: [u8; 32],
|
||||||
|
i: Participant,
|
||||||
|
rng: &mut R,
|
||||||
|
) -> Self {
|
||||||
|
let enc_key = Zeroizing::new(C::random_nonzero_F(rng));
|
||||||
|
Self {
|
||||||
|
context,
|
||||||
|
i,
|
||||||
|
enc_pub_key: C::generator() * enc_key.deref(),
|
||||||
|
enc_key,
|
||||||
|
decryption: Decryption::new(context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn registration<M: Message>(&self, msg: M) -> EncryptionKeyMessage<C, M> {
|
||||||
|
EncryptionKeyMessage { msg, enc_key: self.enc_pub_key }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register<M: Message>(
|
||||||
|
&mut self,
|
||||||
|
participant: Participant,
|
||||||
|
msg: EncryptionKeyMessage<C, M>,
|
||||||
|
) -> M {
|
||||||
|
self.decryption.register(participant, msg)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
|
pub(crate) fn encrypt<R: RngCore + CryptoRng, E: Encryptable>(
|
||||||
&self,
|
&self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
participant: Participant,
|
participant: Participant,
|
||||||
msg: Zeroizing<E>,
|
msg: Zeroizing<E>,
|
||||||
) -> EncryptedMessage<C, E> {
|
) -> EncryptedMessage<C, E> {
|
||||||
encrypt(rng, &self.context, self.i.unwrap(), self.enc_keys[&participant], msg)
|
encrypt(rng, self.context, self.i, self.decryption.enc_keys[&participant], msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
|
pub(crate) fn decrypt<R: RngCore + CryptoRng, I: Copy + Zeroize, E: Encryptable>(
|
||||||
@@ -426,18 +481,18 @@ impl<C: Ciphersuite> Encryption<C> {
|
|||||||
batch,
|
batch,
|
||||||
batch_id,
|
batch_id,
|
||||||
msg.key,
|
msg.key,
|
||||||
pop_challenge::<C>(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
pop_challenge::<C>(self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let key = ecdh::<C>(&self.enc_key, msg.key);
|
let key = ecdh::<C>(&self.enc_key, msg.key);
|
||||||
cipher::<C>(&self.context, &key).apply_keystream(msg.msg.as_mut().as_mut());
|
cipher::<C>(self.context, &key).apply_keystream(msg.msg.as_mut().as_mut());
|
||||||
(
|
(
|
||||||
msg.msg,
|
msg.msg,
|
||||||
EncryptionKeyProof {
|
EncryptionKeyProof {
|
||||||
key,
|
key,
|
||||||
dleq: DLEqProof::prove(
|
dleq: DLEqProof::prove(
|
||||||
rng,
|
rng,
|
||||||
&mut encryption_key_transcript(&self.context),
|
&mut encryption_key_transcript(self.context),
|
||||||
&[C::generator(), msg.key],
|
&[C::generator(), msg.key],
|
||||||
&self.enc_key,
|
&self.enc_key,
|
||||||
),
|
),
|
||||||
@@ -445,38 +500,7 @@ impl<C: Ciphersuite> Encryption<C> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Given a message, and the intended decryptor, and a proof for its key, decrypt the message.
|
pub(crate) fn into_decryption(self) -> Decryption<C> {
|
||||||
// Returns None if the key was wrong.
|
self.decryption
|
||||||
pub(crate) fn decrypt_with_proof<E: Encryptable>(
|
|
||||||
&self,
|
|
||||||
from: Participant,
|
|
||||||
decryptor: Participant,
|
|
||||||
mut msg: EncryptedMessage<C, E>,
|
|
||||||
// There's no encryption key proof if the accusation is of an invalid signature
|
|
||||||
proof: Option<EncryptionKeyProof<C>>,
|
|
||||||
) -> Result<Zeroizing<E>, DecryptionError> {
|
|
||||||
if !msg.pop.verify(
|
|
||||||
msg.key,
|
|
||||||
pop_challenge::<C>(&self.context, msg.pop.R, msg.key, from, msg.msg.deref().as_ref()),
|
|
||||||
) {
|
|
||||||
Err(DecryptionError::InvalidSignature)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(proof) = proof {
|
|
||||||
// Verify this is the decryption key for this message
|
|
||||||
proof
|
|
||||||
.dleq
|
|
||||||
.verify(
|
|
||||||
&mut encryption_key_transcript(&self.context),
|
|
||||||
&[C::generator(), msg.key],
|
|
||||||
&[self.enc_keys[&decryptor], *proof.key],
|
|
||||||
)
|
|
||||||
.map_err(|_| DecryptionError::InvalidProof)?;
|
|
||||||
|
|
||||||
cipher::<C>(&self.context, &proof.key).apply_keystream(msg.msg.as_mut().as_mut());
|
|
||||||
Ok(msg.msg)
|
|
||||||
} else {
|
|
||||||
Err(DecryptionError::InvalidProof)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
// This crate requires `dleq` which doesn't support no-std via std-shims
|
||||||
|
// #![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
use core::{marker::PhantomData, ops::Deref, fmt};
|
use core::{marker::PhantomData, ops::Deref, fmt};
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
|
use multiexp::{multiexp_vartime, BatchVerifier};
|
||||||
use ciphersuite::{
|
use ciphersuite::{
|
||||||
group::{
|
group::{
|
||||||
ff::{Field, PrimeField},
|
ff::{Field, PrimeField},
|
||||||
@@ -17,29 +22,75 @@ use ciphersuite::{
|
|||||||
},
|
},
|
||||||
Ciphersuite,
|
Ciphersuite,
|
||||||
};
|
};
|
||||||
use multiexp::{multiexp_vartime, BatchVerifier};
|
|
||||||
|
|
||||||
use schnorr::SchnorrSignature;
|
use schnorr::SchnorrSignature;
|
||||||
|
|
||||||
use crate::{
|
pub use dkg::*;
|
||||||
Participant, DkgError, ThresholdParams, ThresholdCore, validate_map,
|
|
||||||
encryption::{
|
|
||||||
ReadWrite, EncryptionKeyMessage, EncryptedMessage, Encryption, EncryptionKeyProof,
|
|
||||||
DecryptionError,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type FrostError<C> = DkgError<EncryptionKeyProof<C>>;
|
mod encryption;
|
||||||
|
pub use encryption::*;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
/// Errors possible during key generation.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||||
|
pub enum PedPoPError<C: Ciphersuite> {
|
||||||
|
/// An incorrect amount of participants was provided.
|
||||||
|
#[error("incorrect amount of participants (expected {expected}, found {found})")]
|
||||||
|
IncorrectAmountOfParticipants { expected: usize, found: usize },
|
||||||
|
/// An invalid proof of knowledge was provided.
|
||||||
|
#[error("invalid proof of knowledge (participant {0})")]
|
||||||
|
InvalidCommitments(Participant),
|
||||||
|
/// An invalid DKG share was provided.
|
||||||
|
#[error("invalid share (participant {participant}, blame {blame})")]
|
||||||
|
InvalidShare { participant: Participant, blame: Option<EncryptionKeyProof<C>> },
|
||||||
|
/// A participant was missing.
|
||||||
|
#[error("missing participant {0}")]
|
||||||
|
MissingParticipant(Participant),
|
||||||
|
/// An error propagated from the underlying `dkg` crate.
|
||||||
|
#[error("error from dkg ({0})")]
|
||||||
|
DkgError(DkgError),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate a map of values to have the expected included participants
|
||||||
|
fn validate_map<T, C: Ciphersuite>(
|
||||||
|
map: &HashMap<Participant, T>,
|
||||||
|
included: &[Participant],
|
||||||
|
ours: Participant,
|
||||||
|
) -> Result<(), PedPoPError<C>> {
|
||||||
|
if (map.len() + 1) != included.len() {
|
||||||
|
Err(PedPoPError::IncorrectAmountOfParticipants {
|
||||||
|
expected: included.len(),
|
||||||
|
found: map.len() + 1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for included in included {
|
||||||
|
if *included == ours {
|
||||||
|
if map.contains_key(included) {
|
||||||
|
Err(PedPoPError::DkgError(DkgError::DuplicatedParticipant(*included)))?;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !map.contains_key(included) {
|
||||||
|
Err(PedPoPError::MissingParticipant(*included))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
fn challenge<C: Ciphersuite>(context: &str, l: Participant, R: &[u8], Am: &[u8]) -> C::F {
|
fn challenge<C: Ciphersuite>(context: [u8; 32], l: Participant, R: &[u8], Am: &[u8]) -> C::F {
|
||||||
let mut transcript = RecommendedTranscript::new(b"DKG FROST v0.2");
|
let mut transcript = RecommendedTranscript::new(b"DKG PedPoP v0.2");
|
||||||
transcript.domain_separate(b"schnorr_proof_of_knowledge");
|
transcript.domain_separate(b"schnorr_proof_of_knowledge");
|
||||||
transcript.append_message(b"context", context.as_bytes());
|
transcript.append_message(b"context", context);
|
||||||
transcript.append_message(b"participant", l.to_bytes());
|
transcript.append_message(b"participant", l.to_bytes());
|
||||||
transcript.append_message(b"nonce", R);
|
transcript.append_message(b"nonce", R);
|
||||||
transcript.append_message(b"commitments", Am);
|
transcript.append_message(b"commitments", Am);
|
||||||
C::hash_to_F(b"DKG-FROST-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
C::hash_to_F(b"DKG-PedPoP-proof_of_knowledge-0", &transcript.challenge(b"schnorr"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The commitments message, intended to be broadcast to all other parties.
|
/// The commitments message, intended to be broadcast to all other parties.
|
||||||
@@ -86,19 +137,19 @@ impl<C: Ciphersuite> ReadWrite for Commitments<C> {
|
|||||||
#[derive(Debug, Zeroize)]
|
#[derive(Debug, Zeroize)]
|
||||||
pub struct KeyGenMachine<C: Ciphersuite> {
|
pub struct KeyGenMachine<C: Ciphersuite> {
|
||||||
params: ThresholdParams,
|
params: ThresholdParams,
|
||||||
context: String,
|
context: [u8; 32],
|
||||||
_curve: PhantomData<C>,
|
_curve: PhantomData<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> KeyGenMachine<C> {
|
impl<C: Ciphersuite> KeyGenMachine<C> {
|
||||||
/// Create a new machine to generate a key.
|
/// Create a new machine to generate a key.
|
||||||
///
|
///
|
||||||
/// The context string should be unique among multisigs.
|
/// The context should be unique among multisigs.
|
||||||
pub fn new(params: ThresholdParams, context: String) -> KeyGenMachine<C> {
|
pub fn new(params: ThresholdParams, context: [u8; 32]) -> KeyGenMachine<C> {
|
||||||
KeyGenMachine { params, context, _curve: PhantomData }
|
KeyGenMachine { params, context, _curve: PhantomData }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start generating a key according to the FROST DKG spec.
|
/// Start generating a key according to the PedPoP DKG specification present in the FROST paper.
|
||||||
///
|
///
|
||||||
/// Returns a commitments message to be sent to all parties over an authenticated channel. If any
|
/// Returns a commitments message to be sent to all parties over an authenticated channel. If any
|
||||||
/// party submits multiple sets of commitments, they MUST be treated as malicious.
|
/// party submits multiple sets of commitments, they MUST be treated as malicious.
|
||||||
@@ -106,7 +157,7 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
|
|||||||
self,
|
self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
|
) -> (SecretShareMachine<C>, EncryptionKeyMessage<C, Commitments<C>>) {
|
||||||
let t = usize::from(self.params.t);
|
let t = usize::from(self.params.t());
|
||||||
let mut coefficients = Vec::with_capacity(t);
|
let mut coefficients = Vec::with_capacity(t);
|
||||||
let mut commitments = Vec::with_capacity(t);
|
let mut commitments = Vec::with_capacity(t);
|
||||||
let mut cached_msg = vec![];
|
let mut cached_msg = vec![];
|
||||||
@@ -129,11 +180,11 @@ impl<C: Ciphersuite> KeyGenMachine<C> {
|
|||||||
// There's no reason to spend the time and effort to make this deterministic besides a
|
// There's no reason to spend the time and effort to make this deterministic besides a
|
||||||
// general obsession with canonicity and determinism though
|
// general obsession with canonicity and determinism though
|
||||||
r,
|
r,
|
||||||
challenge::<C>(&self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg),
|
challenge::<C>(self.context, self.params.i(), nonce.to_bytes().as_ref(), &cached_msg),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Additionally create an encryption mechanism to protect the secret shares
|
// Additionally create an encryption mechanism to protect the secret shares
|
||||||
let encryption = Encryption::new(self.context.clone(), Some(self.params.i), rng);
|
let encryption = Encryption::new(self.context, self.params.i(), rng);
|
||||||
|
|
||||||
// Step 4: Broadcast
|
// Step 4: Broadcast
|
||||||
let msg =
|
let msg =
|
||||||
@@ -225,7 +276,7 @@ impl<F: PrimeField> ReadWrite for SecretShare<F> {
|
|||||||
#[derive(Zeroize)]
|
#[derive(Zeroize)]
|
||||||
pub struct SecretShareMachine<C: Ciphersuite> {
|
pub struct SecretShareMachine<C: Ciphersuite> {
|
||||||
params: ThresholdParams,
|
params: ThresholdParams,
|
||||||
context: String,
|
context: [u8; 32],
|
||||||
coefficients: Vec<Zeroizing<C::F>>,
|
coefficients: Vec<Zeroizing<C::F>>,
|
||||||
our_commitments: Vec<C::G>,
|
our_commitments: Vec<C::G>,
|
||||||
encryption: Encryption<C>,
|
encryption: Encryption<C>,
|
||||||
@@ -250,21 +301,21 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
|||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||||
) -> Result<HashMap<Participant, Vec<C::G>>, FrostError<C>> {
|
) -> Result<HashMap<Participant, Vec<C::G>>, PedPoPError<C>> {
|
||||||
validate_map(
|
validate_map(
|
||||||
&commitment_msgs,
|
&commitment_msgs,
|
||||||
&(1 ..= self.params.n()).map(Participant).collect::<Vec<_>>(),
|
&self.params.all_participant_indexes().collect::<Vec<_>>(),
|
||||||
self.params.i(),
|
self.params.i(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut batch = BatchVerifier::<Participant, C::G>::new(commitment_msgs.len());
|
let mut batch = BatchVerifier::<Participant, C::G>::new(commitment_msgs.len());
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
for l in (1 ..= self.params.n()).map(Participant) {
|
for l in self.params.all_participant_indexes() {
|
||||||
let Some(msg) = commitment_msgs.remove(&l) else { continue };
|
let Some(msg) = commitment_msgs.remove(&l) else { continue };
|
||||||
let mut msg = self.encryption.register(l, msg);
|
let mut msg = self.encryption.register(l, msg);
|
||||||
|
|
||||||
if msg.commitments.len() != self.params.t().into() {
|
if msg.commitments.len() != self.params.t().into() {
|
||||||
Err(FrostError::InvalidCommitments(l))?;
|
Err(PedPoPError::InvalidCommitments(l))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 5: Validate each proof of knowledge
|
// Step 5: Validate each proof of knowledge
|
||||||
@@ -274,15 +325,15 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
|||||||
&mut batch,
|
&mut batch,
|
||||||
l,
|
l,
|
||||||
msg.commitments[0],
|
msg.commitments[0],
|
||||||
challenge::<C>(&self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg),
|
challenge::<C>(self.context, l, msg.sig.R.to_bytes().as_ref(), &msg.cached_msg),
|
||||||
);
|
);
|
||||||
|
|
||||||
commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>());
|
commitments.insert(l, msg.commitments.drain(..).collect::<Vec<_>>());
|
||||||
}
|
}
|
||||||
|
|
||||||
batch.verify_vartime_with_vartime_blame().map_err(FrostError::InvalidCommitments)?;
|
batch.verify_vartime_with_vartime_blame().map_err(PedPoPError::InvalidCommitments)?;
|
||||||
|
|
||||||
commitments.insert(self.params.i, self.our_commitments.drain(..).collect());
|
commitments.insert(self.params.i(), self.our_commitments.drain(..).collect());
|
||||||
Ok(commitments)
|
Ok(commitments)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,13 +350,13 @@ impl<C: Ciphersuite> SecretShareMachine<C> {
|
|||||||
commitments: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
commitments: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(KeyMachine<C>, HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>),
|
(KeyMachine<C>, HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>),
|
||||||
FrostError<C>,
|
PedPoPError<C>,
|
||||||
> {
|
> {
|
||||||
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
let commitments = self.verify_r1(&mut *rng, commitments)?;
|
||||||
|
|
||||||
// Step 1: Generate secret shares for all other parties
|
// Step 1: Generate secret shares for all other parties
|
||||||
let mut res = HashMap::new();
|
let mut res = HashMap::new();
|
||||||
for l in (1 ..= self.params.n()).map(Participant) {
|
for l in self.params.all_participant_indexes() {
|
||||||
// Don't insert our own shares to the byte buffer which is meant to be sent around
|
// Don't insert our own shares to the byte buffer which is meant to be sent around
|
||||||
// An app developer could accidentally send it. Best to keep this black boxed
|
// An app developer could accidentally send it. Best to keep this black boxed
|
||||||
if l == self.params.i() {
|
if l == self.params.i() {
|
||||||
@@ -413,10 +464,10 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
|||||||
mut self,
|
mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
mut shares: HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>,
|
mut shares: HashMap<Participant, EncryptedMessage<C, SecretShare<C::F>>>,
|
||||||
) -> Result<BlameMachine<C>, FrostError<C>> {
|
) -> Result<BlameMachine<C>, PedPoPError<C>> {
|
||||||
validate_map(
|
validate_map(
|
||||||
&shares,
|
&shares,
|
||||||
&(1 ..= self.params.n()).map(Participant).collect::<Vec<_>>(),
|
&self.params.all_participant_indexes().collect::<Vec<_>>(),
|
||||||
self.params.i(),
|
self.params.i(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
@@ -427,7 +478,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
|||||||
self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes);
|
self.encryption.decrypt(rng, &mut batch, BatchId::Decryption(l), l, share_bytes);
|
||||||
let share =
|
let share =
|
||||||
Zeroizing::new(Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or_else(|| {
|
Zeroizing::new(Option::<C::F>::from(C::F::from_repr(share_bytes.0)).ok_or_else(|| {
|
||||||
FrostError::InvalidShare { participant: l, blame: Some(blame.clone()) }
|
PedPoPError::InvalidShare { participant: l, blame: Some(blame.clone()) }
|
||||||
})?);
|
})?);
|
||||||
share_bytes.zeroize();
|
share_bytes.zeroize();
|
||||||
*self.secret += share.deref();
|
*self.secret += share.deref();
|
||||||
@@ -444,7 +495,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
|||||||
BatchId::Decryption(l) => (l, None),
|
BatchId::Decryption(l) => (l, None),
|
||||||
BatchId::Share(l) => (l, Some(blames.remove(&l).unwrap())),
|
BatchId::Share(l) => (l, Some(blames.remove(&l).unwrap())),
|
||||||
};
|
};
|
||||||
FrostError::InvalidShare { participant: l, blame }
|
PedPoPError::InvalidShare { participant: l, blame }
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
// Stripe commitments per t and sum them in advance. Calculating verification shares relies on
|
||||||
@@ -458,7 +509,7 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
|||||||
|
|
||||||
// Calculate each user's verification share
|
// Calculate each user's verification share
|
||||||
let mut verification_shares = HashMap::new();
|
let mut verification_shares = HashMap::new();
|
||||||
for i in (1 ..= self.params.n()).map(Participant) {
|
for i in self.params.all_participant_indexes() {
|
||||||
verification_shares.insert(
|
verification_shares.insert(
|
||||||
i,
|
i,
|
||||||
if i == self.params.i() {
|
if i == self.params.i() {
|
||||||
@@ -472,13 +523,11 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
|||||||
let KeyMachine { commitments, encryption, params, secret } = self;
|
let KeyMachine { commitments, encryption, params, secret } = self;
|
||||||
Ok(BlameMachine {
|
Ok(BlameMachine {
|
||||||
commitments,
|
commitments,
|
||||||
encryption,
|
encryption: encryption.into_decryption(),
|
||||||
result: Some(ThresholdCore {
|
result: Some(
|
||||||
params,
|
ThresholdKeys::new(params, Interpolation::Lagrange, secret, verification_shares)
|
||||||
secret_share: secret,
|
.map_err(PedPoPError::DkgError)?,
|
||||||
group_key: stripes[0],
|
),
|
||||||
verification_shares,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,8 +535,8 @@ impl<C: Ciphersuite> KeyMachine<C> {
|
|||||||
/// A machine capable of handling blame proofs.
|
/// A machine capable of handling blame proofs.
|
||||||
pub struct BlameMachine<C: Ciphersuite> {
|
pub struct BlameMachine<C: Ciphersuite> {
|
||||||
commitments: HashMap<Participant, Vec<C::G>>,
|
commitments: HashMap<Participant, Vec<C::G>>,
|
||||||
encryption: Encryption<C>,
|
encryption: Decryption<C>,
|
||||||
result: Option<ThresholdCore<C>>,
|
result: Option<ThresholdKeys<C>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for BlameMachine<C> {
|
impl<C: Ciphersuite> fmt::Debug for BlameMachine<C> {
|
||||||
@@ -505,7 +554,6 @@ impl<C: Ciphersuite> Zeroize for BlameMachine<C> {
|
|||||||
for commitments in self.commitments.values_mut() {
|
for commitments in self.commitments.values_mut() {
|
||||||
commitments.zeroize();
|
commitments.zeroize();
|
||||||
}
|
}
|
||||||
self.encryption.zeroize();
|
|
||||||
self.result.zeroize();
|
self.result.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -520,7 +568,7 @@ impl<C: Ciphersuite> BlameMachine<C> {
|
|||||||
/// territory of consensus protocols. This library does not handle that nor does it provide any
|
/// territory of consensus protocols. This library does not handle that nor does it provide any
|
||||||
/// tooling to do so. This function is solely intended to force users to acknowledge they're
|
/// tooling to do so. This function is solely intended to force users to acknowledge they're
|
||||||
/// completing the protocol, not processing any blame.
|
/// completing the protocol, not processing any blame.
|
||||||
pub fn complete(self) -> ThresholdCore<C> {
|
pub fn complete(self) -> ThresholdKeys<C> {
|
||||||
self.result.unwrap()
|
self.result.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,17 +646,16 @@ impl<C: Ciphersuite> AdditionalBlameMachine<C> {
|
|||||||
/// authenticated as having come from the supposed party and verified as valid. Usage of invalid
|
/// authenticated as having come from the supposed party and verified as valid. Usage of invalid
|
||||||
/// commitments is considered undefined behavior, and may cause everything from inaccurate blame
|
/// commitments is considered undefined behavior, and may cause everything from inaccurate blame
|
||||||
/// to panics.
|
/// to panics.
|
||||||
pub fn new<R: RngCore + CryptoRng>(
|
pub fn new(
|
||||||
rng: &mut R,
|
context: [u8; 32],
|
||||||
context: String,
|
|
||||||
n: u16,
|
n: u16,
|
||||||
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
mut commitment_msgs: HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||||
) -> Result<Self, FrostError<C>> {
|
) -> Result<Self, PedPoPError<C>> {
|
||||||
let mut commitments = HashMap::new();
|
let mut commitments = HashMap::new();
|
||||||
let mut encryption = Encryption::new(context, None, rng);
|
let mut encryption = Decryption::new(context);
|
||||||
for i in 1 ..= n {
|
for i in 1 ..= n {
|
||||||
let i = Participant::new(i).unwrap();
|
let i = Participant::new(i).unwrap();
|
||||||
let Some(msg) = commitment_msgs.remove(&i) else { Err(DkgError::MissingParticipant(i))? };
|
let Some(msg) = commitment_msgs.remove(&i) else { Err(PedPoPError::MissingParticipant(i))? };
|
||||||
commitments.insert(i, encryption.register(i, msg).commitments);
|
commitments.insert(i, encryption.register(i, msg).commitments);
|
||||||
}
|
}
|
||||||
Ok(AdditionalBlameMachine(BlameMachine { commitments, encryption, result: None }))
|
Ok(AdditionalBlameMachine(BlameMachine { commitments, encryption, result: None }))
|
||||||
345
crypto/dkg/pedpop/src/tests.rs
Normal file
345
crypto/dkg/pedpop/src/tests.rs
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng, OsRng};
|
||||||
|
|
||||||
|
use ciphersuite::{Ciphersuite, Ristretto};
|
||||||
|
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
const THRESHOLD: u16 = 3;
|
||||||
|
const PARTICIPANTS: u16 = 5;
|
||||||
|
|
||||||
|
/// Clone a map without a specific value.
|
||||||
|
fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||||
|
map: &HashMap<K, V>,
|
||||||
|
without: &K,
|
||||||
|
) -> HashMap<K, V> {
|
||||||
|
let mut res = map.clone();
|
||||||
|
res.remove(without).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
||||||
|
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
|
||||||
|
|
||||||
|
const CONTEXT: [u8; 32] = *b"DKG Test Key Generation ";
|
||||||
|
|
||||||
|
// Commit, then return commitment messages, enc keys, and shares
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
|
rng: &mut R,
|
||||||
|
) -> (
|
||||||
|
HashMap<Participant, KeyMachine<C>>,
|
||||||
|
HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
||||||
|
HashMap<Participant, C::G>,
|
||||||
|
HashMap<Participant, PedPoPSecretShares<C>>,
|
||||||
|
) {
|
||||||
|
let mut machines = HashMap::new();
|
||||||
|
let mut commitments = HashMap::new();
|
||||||
|
let mut enc_keys = HashMap::new();
|
||||||
|
for i in (1 ..= PARTICIPANTS).map(|i| Participant::new(i).unwrap()) {
|
||||||
|
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
|
||||||
|
let machine = KeyGenMachine::<C>::new(params, CONTEXT);
|
||||||
|
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
||||||
|
machines.insert(i, machine);
|
||||||
|
|
||||||
|
commitments.insert(
|
||||||
|
i,
|
||||||
|
EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params)
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
enc_keys.insert(i, commitments[&i].enc_key());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut secret_shares = HashMap::new();
|
||||||
|
let machines = machines
|
||||||
|
.drain()
|
||||||
|
.map(|(l, machine)| {
|
||||||
|
let (machine, mut shares) =
|
||||||
|
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
|
||||||
|
let shares = shares
|
||||||
|
.drain()
|
||||||
|
.map(|(l, share)| {
|
||||||
|
(
|
||||||
|
l,
|
||||||
|
EncryptedMessage::read::<&[u8]>(
|
||||||
|
&mut share.serialize().as_ref(),
|
||||||
|
// Only t/n actually matters, so hardcode i to 1 here
|
||||||
|
ThresholdParams::new(THRESHOLD, PARTICIPANTS, Participant::new(1).unwrap()).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
secret_shares.insert(l, shares);
|
||||||
|
(l, machine)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
|
(machines, commitments, enc_keys, secret_shares)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_secret_shares<C: Ciphersuite>(
|
||||||
|
shares: &HashMap<Participant, PedPoPSecretShares<C>>,
|
||||||
|
recipient: Participant,
|
||||||
|
) -> PedPoPSecretShares<C> {
|
||||||
|
let mut our_secret_shares = HashMap::new();
|
||||||
|
for (i, shares) in shares {
|
||||||
|
if recipient == *i {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
our_secret_shares.insert(*i, shares[&recipient].clone());
|
||||||
|
}
|
||||||
|
our_secret_shares
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fully perform the PedPoP key generation algorithm.
|
||||||
|
fn pedpop_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
|
rng: &mut R,
|
||||||
|
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||||
|
let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
||||||
|
|
||||||
|
let mut verification_shares = None;
|
||||||
|
let mut group_key = None;
|
||||||
|
machines
|
||||||
|
.drain()
|
||||||
|
.map(|(i, machine)| {
|
||||||
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
|
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
||||||
|
|
||||||
|
// Verify the verification_shares are agreed upon
|
||||||
|
if verification_shares.is_none() {
|
||||||
|
verification_shares = Some(
|
||||||
|
these_keys
|
||||||
|
.params()
|
||||||
|
.all_participant_indexes()
|
||||||
|
.map(|i| (i, these_keys.original_verification_share(i)))
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
verification_shares.as_ref().unwrap(),
|
||||||
|
&these_keys
|
||||||
|
.params()
|
||||||
|
.all_participant_indexes()
|
||||||
|
.map(|i| (i, these_keys.original_verification_share(i)))
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the group keys are agreed upon
|
||||||
|
if group_key.is_none() {
|
||||||
|
group_key = Some(these_keys.group_key());
|
||||||
|
}
|
||||||
|
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
||||||
|
|
||||||
|
(i, these_keys)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ONE: Participant = Participant::new(1).unwrap();
|
||||||
|
const TWO: Participant = Participant::new(2).unwrap();
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_pedpop() {
|
||||||
|
let _ = core::hint::black_box(pedpop_gen::<_, Ristretto>(&mut OsRng));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_blame(
|
||||||
|
commitment_msgs: &HashMap<Participant, EncryptionKeyMessage<Ristretto, Commitments<Ristretto>>>,
|
||||||
|
machines: Vec<BlameMachine<Ristretto>>,
|
||||||
|
msg: &PedPoPEncryptedMessage<Ristretto>,
|
||||||
|
blame: &Option<EncryptionKeyProof<Ristretto>>,
|
||||||
|
) {
|
||||||
|
for machine in machines {
|
||||||
|
let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone());
|
||||||
|
assert_eq!(blamed, ONE);
|
||||||
|
// Verify additional blame also works
|
||||||
|
assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE);
|
||||||
|
|
||||||
|
// Verify machines constructed with AdditionalBlameMachine::new work
|
||||||
|
assert_eq!(
|
||||||
|
AdditionalBlameMachine::new(CONTEXT, PARTICIPANTS, commitment_msgs.clone()).unwrap().blame(
|
||||||
|
ONE,
|
||||||
|
TWO,
|
||||||
|
msg.clone(),
|
||||||
|
blame.clone()
|
||||||
|
),
|
||||||
|
ONE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Write a macro which expands to the following
|
||||||
|
#[test]
|
||||||
|
fn invalid_encryption_pop_blame() {
|
||||||
|
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
// Mutate the PoP of the encrypted message from 1 to 2
|
||||||
|
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop();
|
||||||
|
|
||||||
|
let mut blame = None;
|
||||||
|
let machines = machines
|
||||||
|
.drain()
|
||||||
|
.filter_map(|(i, machine)| {
|
||||||
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
|
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||||
|
if i == TWO {
|
||||||
|
assert_eq!(
|
||||||
|
machine.err(),
|
||||||
|
Some(PedPoPError::InvalidShare { participant: ONE, blame: None })
|
||||||
|
);
|
||||||
|
// Explicitly declare we have a blame object, which happens to be None since invalid PoP
|
||||||
|
// is self-explainable
|
||||||
|
blame = Some(None);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_ecdh_blame() {
|
||||||
|
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
// Mutate the share to trigger a blame event
|
||||||
|
// Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass
|
||||||
|
// While here, 2 is malicious, this is so 1 creates the blame proof
|
||||||
|
// We then malleate 1's blame proof, so 1 ends up malicious
|
||||||
|
// Doesn't simply invalidate the PoP as that won't have a blame statement
|
||||||
|
// By mutating the encrypted data, we do ensure a blame statement is created
|
||||||
|
secret_shares
|
||||||
|
.get_mut(&TWO)
|
||||||
|
.unwrap()
|
||||||
|
.get_mut(&ONE)
|
||||||
|
.unwrap()
|
||||||
|
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||||
|
|
||||||
|
let mut blame = None;
|
||||||
|
let machines = machines
|
||||||
|
.drain()
|
||||||
|
.filter_map(|(i, machine)| {
|
||||||
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
|
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||||
|
if i == ONE {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(PedPoPError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
||||||
|
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should be largely equivalent to the prior test
|
||||||
|
#[test]
|
||||||
|
fn invalid_dleq_blame() {
|
||||||
|
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
secret_shares
|
||||||
|
.get_mut(&TWO)
|
||||||
|
.unwrap()
|
||||||
|
.get_mut(&ONE)
|
||||||
|
.unwrap()
|
||||||
|
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
||||||
|
|
||||||
|
let mut blame = None;
|
||||||
|
let machines = machines
|
||||||
|
.drain()
|
||||||
|
.filter_map(|(i, machine)| {
|
||||||
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
|
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||||
|
if i == ONE {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(PedPoPError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
||||||
|
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_share_serialization_blame() {
|
||||||
|
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization(
|
||||||
|
&mut OsRng,
|
||||||
|
CONTEXT,
|
||||||
|
ONE,
|
||||||
|
enc_keys[&TWO],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut blame = None;
|
||||||
|
let machines = machines
|
||||||
|
.drain()
|
||||||
|
.filter_map(|(i, machine)| {
|
||||||
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
|
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||||
|
if i == TWO {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(PedPoPError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn invalid_share_value_blame() {
|
||||||
|
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
||||||
|
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
||||||
|
|
||||||
|
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value(
|
||||||
|
&mut OsRng,
|
||||||
|
CONTEXT,
|
||||||
|
ONE,
|
||||||
|
enc_keys[&TWO],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut blame = None;
|
||||||
|
let machines = machines
|
||||||
|
.drain()
|
||||||
|
.filter_map(|(i, machine)| {
|
||||||
|
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
||||||
|
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
||||||
|
if i == TWO {
|
||||||
|
blame = Some(match machine.err() {
|
||||||
|
Some(PedPoPError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
||||||
|
_ => panic!(),
|
||||||
|
});
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(machine.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
||||||
|
}
|
||||||
34
crypto/dkg/promote/Cargo.toml
Normal file
34
crypto/dkg/promote/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "dkg-promote"
|
||||||
|
version = "0.6.1"
|
||||||
|
description = "Promotions for keys from the dkg crate"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/promote"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = { version = "2", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
transcript = { package = "flexible-transcript", path = "../../transcript", version = "^0.3.2", default-features = false, features = ["std", "recommended"] }
|
||||||
|
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false, features = ["std"] }
|
||||||
|
dleq = { path = "../../dleq", version = "^0.4.1", default-features = false, features = ["std", "serialize"] }
|
||||||
|
|
||||||
|
dkg = { path = "../", version = "0.6.1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
zeroize = { version = "^1.5", default-features = false, features = ["std", "zeroize_derive"] }
|
||||||
|
rand_core = { version = "0.6", default-features = false, features = ["getrandom"] }
|
||||||
|
ciphersuite = { path = "../../ciphersuite", default-features = false, features = ["ristretto"] }
|
||||||
|
dkg-recovery = { path = "../recovery", default-features = false, features = ["std"] }
|
||||||
21
crypto/dkg/promote/LICENSE
Normal file
21
crypto/dkg/promote/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-2025 Luke Parker
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
13
crypto/dkg/promote/README.md
Normal file
13
crypto/dkg/promote/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Distributed Key Generation - Promote
|
||||||
|
|
||||||
|
This crate implements 'promotions' for keys from the
|
||||||
|
[`dkg`](https://docs.rs/dkg) crate. A promotion takes a set of keys and maps it
|
||||||
|
to a different `Ciphersuite`.
|
||||||
|
|
||||||
|
This crate was originally part of the `dkg` crate, which was
|
||||||
|
[audited by Cypher Stack in March 2023](
|
||||||
|
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||||
|
), culminating in commit
|
||||||
|
[669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||||
|
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||||
|
). Any subsequent changes have not undergone auditing.
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
// This crate requires `dleq` which doesn't support no-std via std-shims
|
||||||
|
// #![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
use core::{marker::PhantomData, ops::Deref};
|
use core::{marker::PhantomData, ops::Deref};
|
||||||
use std::{
|
use std::{
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
sync::Arc,
|
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -12,11 +16,37 @@ use ciphersuite::{group::GroupEncoding, Ciphersuite};
|
|||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
use dleq::DLEqProof;
|
use dleq::DLEqProof;
|
||||||
|
|
||||||
use crate::{Participant, DkgError, ThresholdCore, ThresholdKeys, validate_map};
|
pub use dkg::*;
|
||||||
|
|
||||||
/// Promote a set of keys to another Ciphersuite definition.
|
#[cfg(test)]
|
||||||
pub trait CiphersuitePromote<C2: Ciphersuite> {
|
mod tests;
|
||||||
fn promote(self) -> ThresholdKeys<C2>;
|
|
||||||
|
/// Errors encountered when promoting keys.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||||
|
pub enum PromotionError {
|
||||||
|
/// Invalid participant identifier.
|
||||||
|
#[error("invalid participant (1 <= participant <= {n}, yet participant is {participant})")]
|
||||||
|
InvalidParticipant {
|
||||||
|
/// The total amount of participants.
|
||||||
|
n: u16,
|
||||||
|
/// The specified participant.
|
||||||
|
participant: Participant,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An incorrect amount of participants was specified.
|
||||||
|
#[error("incorrect amount of participants. {t} <= amount <= {n}, yet amount is {amount}")]
|
||||||
|
IncorrectAmountOfParticipants {
|
||||||
|
/// The threshold required.
|
||||||
|
t: u16,
|
||||||
|
/// The total amount of participants.
|
||||||
|
n: u16,
|
||||||
|
/// The amount of participants specified.
|
||||||
|
amount: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Participant provided an invalid proof.
|
||||||
|
#[error("invalid proof {0}")]
|
||||||
|
InvalidProof(Participant),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transcript<G: GroupEncoding>(key: &G, i: Participant) -> RecommendedTranscript {
|
fn transcript<G: GroupEncoding>(key: &G, i: Participant) -> RecommendedTranscript {
|
||||||
@@ -65,20 +95,21 @@ pub struct GeneratorPromotion<C1: Ciphersuite, C2: Ciphersuite> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<C1, C2> {
|
impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<C1, C2> {
|
||||||
/// Begin promoting keys from one generator to another. Returns a proof this share was properly
|
/// Begin promoting keys from one generator to another.
|
||||||
/// promoted.
|
///
|
||||||
|
/// Returns a proof this share was properly promoted.
|
||||||
pub fn promote<R: RngCore + CryptoRng>(
|
pub fn promote<R: RngCore + CryptoRng>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
base: ThresholdKeys<C1>,
|
base: ThresholdKeys<C1>,
|
||||||
) -> (GeneratorPromotion<C1, C2>, GeneratorProof<C1>) {
|
) -> (GeneratorPromotion<C1, C2>, GeneratorProof<C1>) {
|
||||||
// Do a DLEqProof for the new generator
|
// Do a DLEqProof for the new generator
|
||||||
let proof = GeneratorProof {
|
let proof = GeneratorProof {
|
||||||
share: C2::generator() * base.secret_share().deref(),
|
share: C2::generator() * base.original_secret_share().deref(),
|
||||||
proof: DLEqProof::prove(
|
proof: DLEqProof::prove(
|
||||||
rng,
|
rng,
|
||||||
&mut transcript(&base.core.group_key(), base.params().i),
|
&mut transcript(&base.original_group_key(), base.params().i()),
|
||||||
&[C1::generator(), C2::generator()],
|
&[C1::generator(), C2::generator()],
|
||||||
base.secret_share(),
|
base.original_secret_share(),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,34 +120,49 @@ impl<C1: Ciphersuite, C2: Ciphersuite<F = C1::F, G = C1::G>> GeneratorPromotion<
|
|||||||
pub fn complete(
|
pub fn complete(
|
||||||
self,
|
self,
|
||||||
proofs: &HashMap<Participant, GeneratorProof<C1>>,
|
proofs: &HashMap<Participant, GeneratorProof<C1>>,
|
||||||
) -> Result<ThresholdKeys<C2>, DkgError<()>> {
|
) -> Result<ThresholdKeys<C2>, PromotionError> {
|
||||||
let params = self.base.params();
|
let params = self.base.params();
|
||||||
validate_map(proofs, &(1 ..= params.n).map(Participant).collect::<Vec<_>>(), params.i)?;
|
if proofs.len() != (usize::from(params.n()) - 1) {
|
||||||
|
Err(PromotionError::IncorrectAmountOfParticipants {
|
||||||
let original_shares = self.base.verification_shares();
|
t: params.n(),
|
||||||
|
n: params.n(),
|
||||||
|
amount: proofs.len() + 1,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for i in proofs.keys().copied() {
|
||||||
|
if u16::from(i) > params.n() {
|
||||||
|
Err(PromotionError::InvalidParticipant { n: params.n(), participant: i })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut verification_shares = HashMap::new();
|
let mut verification_shares = HashMap::new();
|
||||||
verification_shares.insert(params.i, self.proof.share);
|
verification_shares.insert(params.i(), self.proof.share);
|
||||||
for (i, proof) in proofs {
|
for i in 1 ..= params.n() {
|
||||||
let i = *i;
|
let i = Participant::new(i).unwrap();
|
||||||
|
if i == params.i() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let proof = proofs.get(&i).unwrap();
|
||||||
proof
|
proof
|
||||||
.proof
|
.proof
|
||||||
.verify(
|
.verify(
|
||||||
&mut transcript(&self.base.core.group_key(), i),
|
&mut transcript(&self.base.original_group_key(), i),
|
||||||
&[C1::generator(), C2::generator()],
|
&[C1::generator(), C2::generator()],
|
||||||
&[original_shares[&i], proof.share],
|
&[self.base.original_verification_share(i), proof.share],
|
||||||
)
|
)
|
||||||
.map_err(|_| DkgError::InvalidCommitments(i))?;
|
.map_err(|_| PromotionError::InvalidProof(i))?;
|
||||||
verification_shares.insert(i, proof.share);
|
verification_shares.insert(i, proof.share);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ThresholdKeys {
|
Ok(
|
||||||
core: Arc::new(ThresholdCore::new(
|
ThresholdKeys::new(
|
||||||
params,
|
params,
|
||||||
self.base.secret_share().clone(),
|
self.base.interpolation().clone(),
|
||||||
|
self.base.original_secret_share().clone(),
|
||||||
verification_shares,
|
verification_shares,
|
||||||
)),
|
)
|
||||||
offset: None,
|
.unwrap(),
|
||||||
})
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
116
crypto/dkg/promote/src/tests.rs
Normal file
116
crypto/dkg/promote/src/tests.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
use core::marker::PhantomData;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use ciphersuite::{
|
||||||
|
group::{ff::Field, Group},
|
||||||
|
Ciphersuite, Ristretto,
|
||||||
|
};
|
||||||
|
|
||||||
|
use dkg::*;
|
||||||
|
use dkg_recovery::recover_key;
|
||||||
|
use crate::{GeneratorPromotion, GeneratorProof};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
struct AltGenerator<C: Ciphersuite> {
|
||||||
|
_curve: PhantomData<C>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
||||||
|
type F = C::F;
|
||||||
|
type G = C::G;
|
||||||
|
type H = C::H;
|
||||||
|
|
||||||
|
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
||||||
|
|
||||||
|
fn generator() -> Self::G {
|
||||||
|
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reduce_512(scalar: [u8; 64]) -> Self::F {
|
||||||
|
<C as Ciphersuite>::reduce_512(scalar)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
||||||
|
<C as Ciphersuite>::hash_to_F(dst, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone a map without a specific value.
|
||||||
|
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||||
|
map: &HashMap<K, V>,
|
||||||
|
without: &K,
|
||||||
|
) -> HashMap<K, V> {
|
||||||
|
let mut res = map.clone();
|
||||||
|
res.remove(without).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test promotion of threshold keys to another generator
|
||||||
|
#[test]
|
||||||
|
fn test_generator_promotion() {
|
||||||
|
// Generate a set of `ThresholdKeys`
|
||||||
|
const PARTICIPANTS: u16 = 5;
|
||||||
|
let keys: [ThresholdKeys<_>; PARTICIPANTS as usize] = {
|
||||||
|
let shares: [<Ristretto as Ciphersuite>::F; PARTICIPANTS as usize] =
|
||||||
|
core::array::from_fn(|_| <Ristretto as Ciphersuite>::F::random(&mut OsRng));
|
||||||
|
let verification_shares = (0 .. PARTICIPANTS)
|
||||||
|
.map(|i| {
|
||||||
|
(
|
||||||
|
Participant::new(i + 1).unwrap(),
|
||||||
|
<Ristretto as Ciphersuite>::generator() * shares[usize::from(i)],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<HashMap<_, _>>();
|
||||||
|
core::array::from_fn(|i| {
|
||||||
|
ThresholdKeys::new(
|
||||||
|
ThresholdParams::new(
|
||||||
|
PARTICIPANTS,
|
||||||
|
PARTICIPANTS,
|
||||||
|
Participant::new(u16::try_from(i + 1).unwrap()).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
Interpolation::Constant(vec![<Ristretto as Ciphersuite>::F::ONE; PARTICIPANTS as usize]),
|
||||||
|
Zeroizing::new(shares[i]),
|
||||||
|
verification_shares.clone(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform the promotion
|
||||||
|
let mut promotions = HashMap::new();
|
||||||
|
let mut proofs = HashMap::new();
|
||||||
|
for keys in &keys {
|
||||||
|
let i = keys.params().i();
|
||||||
|
let (promotion, proof) =
|
||||||
|
GeneratorPromotion::<_, AltGenerator<Ristretto>>::promote(&mut OsRng, keys.clone());
|
||||||
|
promotions.insert(i, promotion);
|
||||||
|
proofs.insert(
|
||||||
|
i,
|
||||||
|
GeneratorProof::<Ristretto>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the promotion, and verify it worked
|
||||||
|
let new_group_key = AltGenerator::<Ristretto>::generator() * *recover_key(&keys).unwrap();
|
||||||
|
for (i, promoting) in promotions.drain() {
|
||||||
|
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
||||||
|
assert_eq!(keys[usize::from(u16::from(i) - 1)].params(), promoted.params());
|
||||||
|
assert_eq!(
|
||||||
|
keys[usize::from(u16::from(i) - 1)].original_secret_share(),
|
||||||
|
promoted.original_secret_share()
|
||||||
|
);
|
||||||
|
assert_eq!(new_group_key, promoted.group_key());
|
||||||
|
for l in 0 .. PARTICIPANTS {
|
||||||
|
let verification_share =
|
||||||
|
promoted.original_verification_share(Participant::new(l + 1).unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
AltGenerator::<Ristretto>::generator() * **keys[usize::from(l)].original_secret_share(),
|
||||||
|
verification_share
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
crypto/dkg/recovery/Cargo.toml
Normal file
34
crypto/dkg/recovery/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "dkg-recovery"
|
||||||
|
version = "0.6.0"
|
||||||
|
description = "Recover a secret-shared key from a collection of dkg::ThresholdKeys"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg/recovery"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
keywords = ["dkg", "multisig", "threshold", "ff", "group"]
|
||||||
|
edition = "2021"
|
||||||
|
rust-version = "1.80"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
zeroize = { version = "^1.5", default-features = false }
|
||||||
|
|
||||||
|
thiserror = { version = "2", default-features = false }
|
||||||
|
|
||||||
|
ciphersuite = { path = "../../ciphersuite", version = "^0.4.1", default-features = false }
|
||||||
|
dkg = { path = "../", version = "0.6", default-features = false }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
std = [
|
||||||
|
"zeroize/std",
|
||||||
|
"thiserror/std",
|
||||||
|
"ciphersuite/std",
|
||||||
|
"dkg/std",
|
||||||
|
]
|
||||||
|
default = ["std"]
|
||||||
21
crypto/dkg/recovery/LICENSE
Normal file
21
crypto/dkg/recovery/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021-2025 Luke Parker
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
14
crypto/dkg/recovery/README.md
Normal file
14
crypto/dkg/recovery/README.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Distributed Key Generation - Recovery
|
||||||
|
|
||||||
|
A utility function to recover a key from its secret shares.
|
||||||
|
|
||||||
|
Keys likely SHOULD NOT ever be recovered, making this primarily intended for
|
||||||
|
testing purposes. Instead, the shares of the key should be used to produce
|
||||||
|
shares for the desired action, allowing using the key while never
|
||||||
|
reconstructing it.
|
||||||
|
|
||||||
|
Before being smashed, this crate was [audited by Cypher Stack in March 2023](
|
||||||
|
https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf
|
||||||
|
), culminating in commit [669d2dbffc1dafb82a09d9419ea182667115df06](
|
||||||
|
https://github.com/serai-dex/serai/tree/669d2dbffc1dafb82a09d9419ea182667115df06
|
||||||
|
). Any subsequent changes have not undergone auditing.
|
||||||
85
crypto/dkg/recovery/src/lib.rs
Normal file
85
crypto/dkg/recovery/src/lib.rs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![doc = include_str!("../README.md")]
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
use core::ops::{Deref, DerefMut};
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use ciphersuite::Ciphersuite;
|
||||||
|
|
||||||
|
pub use dkg::*;
|
||||||
|
|
||||||
|
/// Errors encountered when recovering a secret-shared key from a collection of
|
||||||
|
/// `dkg::ThresholdKeys`.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||||
|
pub enum RecoveryError {
|
||||||
|
/// No keys were provided.
|
||||||
|
#[error("no keys provided")]
|
||||||
|
NoKeysProvided,
|
||||||
|
/// Not enough keys were provided.
|
||||||
|
#[error("not enough keys provided (threshold required {required}, provided {provided})")]
|
||||||
|
NotEnoughKeysProvided { required: u16, provided: usize },
|
||||||
|
/// The keys had inconsistent parameters.
|
||||||
|
#[error("keys had inconsistent parameters")]
|
||||||
|
InconsistentParameters,
|
||||||
|
/// The keys are from distinct secret-sharing sessions or otherwise corrupt.
|
||||||
|
#[error("recovery failed")]
|
||||||
|
Failure,
|
||||||
|
/// An error propagated from the underlying `dkg` crate.
|
||||||
|
#[error("error from dkg ({0})")]
|
||||||
|
DkgError(DkgError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recover a shared secret from a collection of `dkg::ThresholdKeys`.
|
||||||
|
pub fn recover_key<C: Ciphersuite>(
|
||||||
|
keys: &[ThresholdKeys<C>],
|
||||||
|
) -> Result<Zeroizing<C::F>, RecoveryError> {
|
||||||
|
let included = keys.iter().map(|keys| keys.params().i()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let keys_len = keys.len();
|
||||||
|
let mut keys = keys.iter();
|
||||||
|
let first_keys = keys.next().ok_or(RecoveryError::NoKeysProvided)?;
|
||||||
|
{
|
||||||
|
let t = first_keys.params().t();
|
||||||
|
if keys_len < usize::from(t) {
|
||||||
|
Err(RecoveryError::NotEnoughKeysProvided { required: t, provided: keys_len })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let first_params = (
|
||||||
|
first_keys.params().t(),
|
||||||
|
first_keys.params().n(),
|
||||||
|
first_keys.group_key(),
|
||||||
|
first_keys.current_scalar(),
|
||||||
|
first_keys.current_offset(),
|
||||||
|
);
|
||||||
|
for keys in keys.clone() {
|
||||||
|
let params = (
|
||||||
|
keys.params().t(),
|
||||||
|
keys.params().n(),
|
||||||
|
keys.group_key(),
|
||||||
|
keys.current_scalar(),
|
||||||
|
keys.current_offset(),
|
||||||
|
);
|
||||||
|
if params != first_params {
|
||||||
|
Err(RecoveryError::InconsistentParameters)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut res: Zeroizing<_> =
|
||||||
|
first_keys.view(included.clone()).map_err(RecoveryError::DkgError)?.secret_share().clone();
|
||||||
|
for keys in keys {
|
||||||
|
*res.deref_mut() +=
|
||||||
|
keys.view(included.clone()).map_err(RecoveryError::DkgError)?.secret_share().deref();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (C::generator() * res.deref()) != first_keys.group_key() {
|
||||||
|
Err(RecoveryError::Failure)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
@@ -2,40 +2,29 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
use core::fmt::{self, Debug};
|
use core::{
|
||||||
|
ops::Deref,
|
||||||
|
fmt::{self, Debug},
|
||||||
|
};
|
||||||
|
use std_shims::{sync::Arc, vec, vec::Vec, collections::HashMap, io};
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
use ciphersuite::{
|
||||||
|
group::{
|
||||||
/// MuSig-style key aggregation.
|
ff::{Field, PrimeField},
|
||||||
pub mod musig;
|
GroupEncoding,
|
||||||
|
},
|
||||||
/// Encryption types and utilities used to secure DKG messages.
|
Ciphersuite,
|
||||||
#[cfg(feature = "std")]
|
};
|
||||||
pub mod encryption;
|
|
||||||
|
|
||||||
/// The PedPoP distributed key generation protocol described in the
|
|
||||||
/// [FROST paper](https://eprint.iacr.org/2020/852), augmented to be verifiable.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod pedpop;
|
|
||||||
|
|
||||||
/// Promote keys between ciphersuites.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod promote;
|
|
||||||
|
|
||||||
/// Tests for application-provided curves and algorithms.
|
|
||||||
#[cfg(any(test, feature = "tests"))]
|
|
||||||
pub mod tests;
|
|
||||||
|
|
||||||
/// The ID of a participant, defined as a non-zero u16.
|
/// The ID of a participant, defined as a non-zero u16.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Zeroize)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Zeroize)]
|
||||||
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
|
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
|
||||||
pub struct Participant(pub(crate) u16);
|
pub struct Participant(u16);
|
||||||
impl Participant {
|
impl Participant {
|
||||||
/// Create a new Participant identifier from a u16.
|
/// Create a new Participant identifier from a u16.
|
||||||
pub fn new(i: u16) -> Option<Participant> {
|
pub const fn new(i: u16) -> Option<Participant> {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -45,7 +34,7 @@ impl Participant {
|
|||||||
|
|
||||||
/// Convert a Participant identifier to bytes.
|
/// Convert a Participant identifier to bytes.
|
||||||
#[allow(clippy::wrong_self_convention)]
|
#[allow(clippy::wrong_self_convention)]
|
||||||
pub fn to_bytes(&self) -> [u8; 2] {
|
pub const fn to_bytes(&self) -> [u8; 2] {
|
||||||
self.0.to_le_bytes()
|
self.0.to_le_bytes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,151 +51,180 @@ impl fmt::Display for Participant {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Various errors possible during key generation.
|
/// Errors encountered when working with threshold keys.
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
|
||||||
#[cfg_attr(feature = "std", derive(Error))]
|
pub enum DkgError {
|
||||||
pub enum DkgError<B: Clone + PartialEq + Eq + Debug> {
|
|
||||||
/// A parameter was zero.
|
/// A parameter was zero.
|
||||||
#[cfg_attr(feature = "std", error("a parameter was 0 (threshold {0}, participants {1})"))]
|
#[error("a parameter was 0 (threshold {t}, participants {n})")]
|
||||||
ZeroParameter(u16, u16),
|
ZeroParameter {
|
||||||
|
/// The specified threshold.
|
||||||
|
t: u16,
|
||||||
|
/// The specified total amount of participants.
|
||||||
|
n: u16,
|
||||||
|
},
|
||||||
|
|
||||||
/// The threshold exceeded the amount of participants.
|
/// The threshold exceeded the amount of participants.
|
||||||
#[cfg_attr(feature = "std", error("invalid threshold (max {1}, got {0})"))]
|
#[error("invalid threshold (max {n}, got {t})")]
|
||||||
InvalidThreshold(u16, u16),
|
InvalidThreshold {
|
||||||
|
/// The specified threshold.
|
||||||
|
t: u16,
|
||||||
|
/// The specified total amount of participants.
|
||||||
|
n: u16,
|
||||||
|
},
|
||||||
|
|
||||||
/// Invalid participant identifier.
|
/// Invalid participant identifier.
|
||||||
#[cfg_attr(
|
#[error("invalid participant (1 <= participant <= {n}, yet participant is {participant})")]
|
||||||
feature = "std",
|
InvalidParticipant {
|
||||||
error("invalid participant (0 < participant <= {0}, yet participant is {1})")
|
/// The total amount of participants.
|
||||||
)]
|
n: u16,
|
||||||
InvalidParticipant(u16, Participant),
|
/// The specified participant.
|
||||||
|
participant: Participant,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An incorrect amount of participants was specified.
|
||||||
|
#[error("incorrect amount of verification shares (n = {n} yet {shares} provided)")]
|
||||||
|
IncorrectAmountOfVerificationShares {
|
||||||
|
/// The amount of participants.
|
||||||
|
n: u16,
|
||||||
|
/// The amount of shares provided.
|
||||||
|
shares: usize,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// An inapplicable method of interpolation was specified.
|
||||||
|
#[error("inapplicable method of interpolation ({0})")]
|
||||||
|
InapplicableInterpolation(&'static str),
|
||||||
|
|
||||||
|
/// An incorrect amount of participants was specified.
|
||||||
|
#[error("incorrect amount of participants. {t} <= amount <= {n}, yet amount is {amount}")]
|
||||||
|
IncorrectAmountOfParticipants {
|
||||||
|
/// The threshold required.
|
||||||
|
t: u16,
|
||||||
|
/// The total amount of participants.
|
||||||
|
n: u16,
|
||||||
|
/// The amount of participants specified.
|
||||||
|
amount: usize,
|
||||||
|
},
|
||||||
|
|
||||||
/// Invalid signing set.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid signing set"))]
|
|
||||||
InvalidSigningSet,
|
|
||||||
/// Invalid amount of participants.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid participant quantity (expected {0}, got {1})"))]
|
|
||||||
InvalidParticipantQuantity(usize, usize),
|
|
||||||
/// A participant was duplicated.
|
/// A participant was duplicated.
|
||||||
#[cfg_attr(feature = "std", error("duplicated participant ({0})"))]
|
#[error("a participant ({0}) was duplicated")]
|
||||||
DuplicatedParticipant(Participant),
|
DuplicatedParticipant(Participant),
|
||||||
/// A participant was missing.
|
|
||||||
#[cfg_attr(feature = "std", error("missing participant {0}"))]
|
|
||||||
MissingParticipant(Participant),
|
|
||||||
|
|
||||||
/// An invalid proof of knowledge was provided.
|
/// Not participating in declared signing set.
|
||||||
#[cfg_attr(feature = "std", error("invalid proof of knowledge (participant {0})"))]
|
#[error("not participating in declared signing set")]
|
||||||
InvalidCommitments(Participant),
|
NotParticipating,
|
||||||
/// An invalid DKG share was provided.
|
|
||||||
#[cfg_attr(feature = "std", error("invalid share (participant {participant}, blame {blame})"))]
|
|
||||||
InvalidShare { participant: Participant, blame: Option<B> },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
// Manually implements BorshDeserialize so we can enforce it's a valid index
|
||||||
mod lib {
|
#[cfg(feature = "borsh")]
|
||||||
pub use super::*;
|
impl borsh::BorshDeserialize for Participant {
|
||||||
|
|
||||||
use core::ops::Deref;
|
|
||||||
use std::{io, sync::Arc, collections::HashMap};
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
use ciphersuite::{
|
|
||||||
group::{
|
|
||||||
ff::{Field, PrimeField},
|
|
||||||
GroupEncoding,
|
|
||||||
},
|
|
||||||
Ciphersuite,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "borsh")]
|
|
||||||
impl borsh::BorshDeserialize for Participant {
|
|
||||||
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
Participant::new(u16::deserialize_reader(reader)?)
|
Participant::new(u16::deserialize_reader(reader)?)
|
||||||
.ok_or_else(|| io::Error::other("invalid participant"))
|
.ok_or_else(|| io::Error::other("invalid participant"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate a map of values to have the expected included participants
|
/// Parameters for a multisig.
|
||||||
pub(crate) fn validate_map<T, B: Clone + PartialEq + Eq + Debug>(
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
map: &HashMap<Participant, T>,
|
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
|
||||||
included: &[Participant],
|
pub struct ThresholdParams {
|
||||||
ours: Participant,
|
|
||||||
) -> Result<(), DkgError<B>> {
|
|
||||||
if (map.len() + 1) != included.len() {
|
|
||||||
Err(DkgError::InvalidParticipantQuantity(included.len(), map.len() + 1))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for included in included {
|
|
||||||
if *included == ours {
|
|
||||||
if map.contains_key(included) {
|
|
||||||
Err(DkgError::DuplicatedParticipant(*included))?;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !map.contains_key(included) {
|
|
||||||
Err(DkgError::MissingParticipant(*included))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parameters for a multisig.
|
|
||||||
// These fields should not be made public as they should be static
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize))]
|
|
||||||
pub struct ThresholdParams {
|
|
||||||
/// Participants needed to sign on behalf of the group.
|
/// Participants needed to sign on behalf of the group.
|
||||||
pub(crate) t: u16,
|
t: u16,
|
||||||
/// Amount of participants.
|
/// Amount of participants.
|
||||||
pub(crate) n: u16,
|
n: u16,
|
||||||
/// Index of the participant being acted for.
|
/// Index of the participant being acted for.
|
||||||
pub(crate) i: Participant,
|
i: Participant,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over all participant indexes.
|
||||||
|
struct AllParticipantIndexes {
|
||||||
|
i: u16,
|
||||||
|
n: u16,
|
||||||
|
}
|
||||||
|
impl Iterator for AllParticipantIndexes {
|
||||||
|
type Item = Participant;
|
||||||
|
fn next(&mut self) -> Option<Participant> {
|
||||||
|
if self.i > self.n {
|
||||||
|
None?;
|
||||||
|
}
|
||||||
|
let res = Participant::new(self.i).unwrap();
|
||||||
|
|
||||||
|
// If i == n == u16::MAX, we cause `i > n` by setting `n` to `0` so the iterator becomes empty
|
||||||
|
if self.i == u16::MAX {
|
||||||
|
self.n = 0;
|
||||||
|
} else {
|
||||||
|
self.i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThresholdParams {
|
Some(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThresholdParams {
|
||||||
/// Create a new set of parameters.
|
/// Create a new set of parameters.
|
||||||
pub fn new(t: u16, n: u16, i: Participant) -> Result<ThresholdParams, DkgError<()>> {
|
pub const fn new(t: u16, n: u16, i: Participant) -> Result<ThresholdParams, DkgError> {
|
||||||
if (t == 0) || (n == 0) {
|
if (t == 0) || (n == 0) {
|
||||||
Err(DkgError::ZeroParameter(t, n))?;
|
return Err(DkgError::ZeroParameter { t, n });
|
||||||
}
|
}
|
||||||
|
|
||||||
if t > n {
|
if t > n {
|
||||||
Err(DkgError::InvalidThreshold(t, n))?;
|
return Err(DkgError::InvalidThreshold { t, n });
|
||||||
}
|
}
|
||||||
if u16::from(i) > n {
|
if i.0 > n {
|
||||||
Err(DkgError::InvalidParticipant(n, i))?;
|
return Err(DkgError::InvalidParticipant { n, participant: i });
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ThresholdParams { t, n, i })
|
Ok(ThresholdParams { t, n, i })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the threshold for a multisig with these parameters.
|
/// The threshold for a multisig with these parameters.
|
||||||
pub fn t(&self) -> u16 {
|
pub const fn t(&self) -> u16 {
|
||||||
self.t
|
self.t
|
||||||
}
|
}
|
||||||
/// Return the amount of participants for a multisig with these parameters.
|
/// The amount of participants for a multisig with these parameters.
|
||||||
pub fn n(&self) -> u16 {
|
pub const fn n(&self) -> u16 {
|
||||||
self.n
|
self.n
|
||||||
}
|
}
|
||||||
/// Return the participant index of the share with these parameters.
|
/// The participant index of the share with these parameters.
|
||||||
pub fn i(&self) -> Participant {
|
pub const fn i(&self) -> Participant {
|
||||||
self.i
|
self.i
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "borsh")]
|
/// An iterator over all participant indexes.
|
||||||
impl borsh::BorshDeserialize for ThresholdParams {
|
pub fn all_participant_indexes(&self) -> impl Iterator<Item = Participant> {
|
||||||
|
AllParticipantIndexes { i: 1, n: self.n }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "borsh")]
|
||||||
|
impl borsh::BorshDeserialize for ThresholdParams {
|
||||||
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
||||||
let t = u16::deserialize_reader(reader)?;
|
let t = u16::deserialize_reader(reader)?;
|
||||||
let n = u16::deserialize_reader(reader)?;
|
let n = u16::deserialize_reader(reader)?;
|
||||||
let i = Participant::deserialize_reader(reader)?;
|
let i = Participant::deserialize_reader(reader)?;
|
||||||
ThresholdParams::new(t, n, i).map_err(|e| io::Error::other(format!("{e:?}")))
|
ThresholdParams::new(t, n, i).map_err(|e| io::Error::other(format!("{e:?}")))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate the lagrange coefficient for a signing set.
|
/// A method of interpolation.
|
||||||
pub fn lagrange<F: PrimeField>(i: Participant, included: &[Participant]) -> F {
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum Interpolation<F: Zeroize + PrimeField> {
|
||||||
|
/// A list of constant coefficients, one for each of the secret key shares.
|
||||||
|
/*
|
||||||
|
There's no benefit to using a full linear combination here, as the additive term would have
|
||||||
|
an entirely known evaluation with a fixed, public coefficient of `1`. Accordingly, the entire
|
||||||
|
key can simply be offset with the additive term to achieve the same effect.
|
||||||
|
*/
|
||||||
|
Constant(Vec<F>),
|
||||||
|
/// Lagrange interpolation.
|
||||||
|
Lagrange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Zeroize + PrimeField> Interpolation<F> {
|
||||||
|
/// The interpolation factor for this participant, within this signing set.
|
||||||
|
fn interpolation_factor(&self, i: Participant, included: &[Participant]) -> F {
|
||||||
|
match self {
|
||||||
|
Interpolation::Constant(c) => c[usize::from(u16::from(i) - 1)],
|
||||||
|
Interpolation::Lagrange => {
|
||||||
let i_f = F::from(u64::from(u16::from(i)));
|
let i_f = F::from(u64::from(u16::from(i)));
|
||||||
|
|
||||||
let mut num = F::ONE;
|
let mut num = F::ONE;
|
||||||
@@ -225,106 +243,335 @@ mod lib {
|
|||||||
// (which we have an if case to avoid)
|
// (which we have an if case to avoid)
|
||||||
num * denom.invert().unwrap()
|
num * denom.invert().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keys and verification shares generated by a DKG.
|
|
||||||
/// Called core as they're expected to be wrapped into an Arc before usage in various operations.
|
|
||||||
#[derive(Clone, PartialEq, Eq)]
|
|
||||||
pub struct ThresholdCore<C: Ciphersuite> {
|
|
||||||
/// Threshold Parameters.
|
|
||||||
pub(crate) params: ThresholdParams,
|
|
||||||
|
|
||||||
/// Secret share key.
|
|
||||||
pub(crate) secret_share: Zeroizing<C::F>,
|
|
||||||
/// Group key.
|
|
||||||
pub(crate) group_key: C::G,
|
|
||||||
/// Verification shares.
|
|
||||||
pub(crate) verification_shares: HashMap<Participant, C::G>,
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for ThresholdCore<C> {
|
/// A key share for a thresholdized secret key.
|
||||||
|
///
|
||||||
|
/// This is the 'core' structure containing all relevant data, expected to be wrapped into an
|
||||||
|
/// heap-allocated pointer to minimize copies on the stack (`ThresholdKeys`, the publicly exposed
|
||||||
|
/// type).
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
struct ThresholdCore<C: Ciphersuite> {
|
||||||
|
params: ThresholdParams,
|
||||||
|
group_key: C::G,
|
||||||
|
verification_shares: HashMap<Participant, C::G>,
|
||||||
|
interpolation: Interpolation<C::F>,
|
||||||
|
secret_share: Zeroizing<C::F>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> fmt::Debug for ThresholdCore<C> {
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt
|
fmt
|
||||||
.debug_struct("ThresholdCore")
|
.debug_struct("ThresholdCore")
|
||||||
.field("params", &self.params)
|
.field("params", &self.params)
|
||||||
.field("group_key", &self.group_key)
|
.field("group_key", &self.group_key)
|
||||||
.field("verification_shares", &self.verification_shares)
|
.field("verification_shares", &self.verification_shares)
|
||||||
|
.field("interpolation", &self.interpolation)
|
||||||
.finish_non_exhaustive()
|
.finish_non_exhaustive()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
|
impl<C: Ciphersuite> Zeroize for ThresholdCore<C> {
|
||||||
fn zeroize(&mut self) {
|
fn zeroize(&mut self) {
|
||||||
self.params.zeroize();
|
self.params.zeroize();
|
||||||
self.secret_share.zeroize();
|
|
||||||
self.group_key.zeroize();
|
self.group_key.zeroize();
|
||||||
for share in self.verification_shares.values_mut() {
|
for share in self.verification_shares.values_mut() {
|
||||||
share.zeroize();
|
share.zeroize();
|
||||||
}
|
}
|
||||||
|
self.interpolation.zeroize();
|
||||||
|
self.secret_share.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C: Ciphersuite> ThresholdCore<C> {
|
/// Threshold keys usable for signing.
|
||||||
pub(crate) fn new(
|
#[derive(Clone, Debug, Zeroize)]
|
||||||
|
pub struct ThresholdKeys<C: Ciphersuite> {
|
||||||
|
// Core keys.
|
||||||
|
#[zeroize(skip)]
|
||||||
|
core: Arc<Zeroizing<ThresholdCore<C>>>,
|
||||||
|
|
||||||
|
// Scalar applied to these keys.
|
||||||
|
scalar: C::F,
|
||||||
|
// Offset applied to these keys.
|
||||||
|
offset: C::F,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View of keys, interpolated and with the expected linear combination taken for usage.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ThresholdView<C: Ciphersuite> {
|
||||||
|
interpolation: Interpolation<C::F>,
|
||||||
|
scalar: C::F,
|
||||||
|
offset: C::F,
|
||||||
|
group_key: C::G,
|
||||||
|
included: Vec<Participant>,
|
||||||
|
secret_share: Zeroizing<C::F>,
|
||||||
|
original_verification_shares: HashMap<Participant, C::G>,
|
||||||
|
verification_shares: HashMap<Participant, C::G>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> fmt::Debug for ThresholdView<C> {
|
||||||
|
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt
|
||||||
|
.debug_struct("ThresholdView")
|
||||||
|
.field("interpolation", &self.interpolation)
|
||||||
|
.field("scalar", &self.scalar)
|
||||||
|
.field("offset", &self.offset)
|
||||||
|
.field("group_key", &self.group_key)
|
||||||
|
.field("included", &self.included)
|
||||||
|
.field("original_verification_shares", &self.original_verification_shares)
|
||||||
|
.field("verification_shares", &self.verification_shares)
|
||||||
|
.finish_non_exhaustive()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> Zeroize for ThresholdView<C> {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.scalar.zeroize();
|
||||||
|
self.offset.zeroize();
|
||||||
|
self.group_key.zeroize();
|
||||||
|
self.included.zeroize();
|
||||||
|
self.secret_share.zeroize();
|
||||||
|
for share in self.original_verification_shares.values_mut() {
|
||||||
|
share.zeroize();
|
||||||
|
}
|
||||||
|
for share in self.verification_shares.values_mut() {
|
||||||
|
share.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> ThresholdKeys<C> {
|
||||||
|
/// Create a new set of ThresholdKeys.
|
||||||
|
pub fn new(
|
||||||
params: ThresholdParams,
|
params: ThresholdParams,
|
||||||
|
interpolation: Interpolation<C::F>,
|
||||||
secret_share: Zeroizing<C::F>,
|
secret_share: Zeroizing<C::F>,
|
||||||
verification_shares: HashMap<Participant, C::G>,
|
verification_shares: HashMap<Participant, C::G>,
|
||||||
) -> ThresholdCore<C> {
|
) -> Result<ThresholdKeys<C>, DkgError> {
|
||||||
|
if verification_shares.len() != usize::from(params.n()) {
|
||||||
|
Err(DkgError::IncorrectAmountOfVerificationShares {
|
||||||
|
n: params.n(),
|
||||||
|
shares: verification_shares.len(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
for participant in verification_shares.keys().copied() {
|
||||||
|
if u16::from(participant) > params.n() {
|
||||||
|
Err(DkgError::InvalidParticipant { n: params.n(), participant })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match &interpolation {
|
||||||
|
Interpolation::Constant(_) => {
|
||||||
|
if params.t() != params.n() {
|
||||||
|
Err(DkgError::InapplicableInterpolation("constant interpolation for keys where t != n"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Interpolation::Lagrange => {}
|
||||||
|
}
|
||||||
|
|
||||||
let t = (1 ..= params.t()).map(Participant).collect::<Vec<_>>();
|
let t = (1 ..= params.t()).map(Participant).collect::<Vec<_>>();
|
||||||
ThresholdCore {
|
let group_key =
|
||||||
|
t.iter().map(|i| verification_shares[i] * interpolation.interpolation_factor(*i, &t)).sum();
|
||||||
|
|
||||||
|
Ok(ThresholdKeys {
|
||||||
|
core: Arc::new(Zeroizing::new(ThresholdCore {
|
||||||
params,
|
params,
|
||||||
|
interpolation,
|
||||||
secret_share,
|
secret_share,
|
||||||
group_key: t.iter().map(|i| verification_shares[i] * lagrange::<C::F>(*i, &t)).sum(),
|
group_key,
|
||||||
verification_shares,
|
verification_shares,
|
||||||
}
|
})),
|
||||||
|
scalar: C::F::ONE,
|
||||||
|
offset: C::F::ZERO,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parameters for these keys.
|
/// Scale the keys by a given scalar to allow for various account and privacy schemes.
|
||||||
|
///
|
||||||
|
/// This scalar is ephemeral and will not be included when these keys are serialized. The
|
||||||
|
/// scalar is applied on top of any already-existing scalar/offset.
|
||||||
|
///
|
||||||
|
/// Returns `None` if the scalar is equal to `0`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn scale(mut self, scalar: C::F) -> Option<ThresholdKeys<C>> {
|
||||||
|
if bool::from(scalar.is_zero()) {
|
||||||
|
None?;
|
||||||
|
}
|
||||||
|
self.scalar *= scalar;
|
||||||
|
self.offset *= scalar;
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offset the keys by a given scalar to allow for various account and privacy schemes.
|
||||||
|
///
|
||||||
|
/// This offset is ephemeral and will not be included when these keys are serialized. The
|
||||||
|
/// offset is applied on top of any already-existing scalar/offset.
|
||||||
|
#[must_use]
|
||||||
|
pub fn offset(mut self, offset: C::F) -> ThresholdKeys<C> {
|
||||||
|
self.offset += offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the current scalar in-use for these keys.
|
||||||
|
pub fn current_scalar(&self) -> C::F {
|
||||||
|
self.scalar
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the current offset in-use for these keys.
|
||||||
|
pub fn current_offset(&self) -> C::F {
|
||||||
|
self.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the parameters for these keys.
|
||||||
pub fn params(&self) -> ThresholdParams {
|
pub fn params(&self) -> ThresholdParams {
|
||||||
self.params
|
self.core.params
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Secret share for these keys.
|
/// Return the original group key, without any tweaks applied.
|
||||||
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
pub fn original_group_key(&self) -> C::G {
|
||||||
&self.secret_share
|
self.core.group_key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Group key for these keys.
|
/// Return the interpolation method for these keys.
|
||||||
|
pub fn interpolation(&self) -> &Interpolation<C::F> {
|
||||||
|
&self.core.interpolation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the group key, with the expected linear combination taken.
|
||||||
pub fn group_key(&self) -> C::G {
|
pub fn group_key(&self) -> C::G {
|
||||||
self.group_key
|
(self.core.group_key * self.scalar) + (C::generator() * self.offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn verification_shares(&self) -> HashMap<Participant, C::G> {
|
/// Return the underlying secret share for these keys, without any tweaks applied.
|
||||||
self.verification_shares.clone()
|
pub fn original_secret_share(&self) -> &Zeroizing<C::F> {
|
||||||
|
&self.core.secret_share
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write these keys to a type satisfying std::io::Write.
|
/// Return the original (untweaked) verification share for the specified participant.
|
||||||
|
///
|
||||||
|
/// This will panic if the participant index is invalid for these keys.
|
||||||
|
pub fn original_verification_share(&self, l: Participant) -> C::G {
|
||||||
|
self.core.verification_shares[&l]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain a view of these keys, interpolated for the specified signing set, with the specified
|
||||||
|
/// linear combination taken.
|
||||||
|
pub fn view(&self, mut included: Vec<Participant>) -> Result<ThresholdView<C>, DkgError> {
|
||||||
|
if (included.len() < self.params().t.into()) ||
|
||||||
|
(usize::from(self.params().n()) < included.len())
|
||||||
|
{
|
||||||
|
Err(DkgError::IncorrectAmountOfParticipants {
|
||||||
|
t: self.params().t,
|
||||||
|
n: self.params().n,
|
||||||
|
amount: included.len(),
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
included.sort();
|
||||||
|
{
|
||||||
|
let mut found = included[0] == self.params().i();
|
||||||
|
for i in 1 .. included.len() {
|
||||||
|
if included[i - 1] == included[i] {
|
||||||
|
Err(DkgError::DuplicatedParticipant(included[i]))?;
|
||||||
|
}
|
||||||
|
found |= included[i] == self.params().i();
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
Err(DkgError::NotParticipating)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let last = *included.last().unwrap();
|
||||||
|
if u16::from(last) > self.params().n() {
|
||||||
|
Err(DkgError::InvalidParticipant { n: self.params().n(), participant: last })?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The interpolation occurs multiplicatively, letting us scale by the scalar now
|
||||||
|
let secret_share_scaled = Zeroizing::new(self.scalar * self.original_secret_share().deref());
|
||||||
|
let mut secret_share = Zeroizing::new(
|
||||||
|
self.core.interpolation.interpolation_factor(self.params().i(), &included) *
|
||||||
|
secret_share_scaled.deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut verification_shares = HashMap::with_capacity(included.len());
|
||||||
|
for i in &included {
|
||||||
|
let verification_share = self.core.verification_shares[i];
|
||||||
|
let verification_share = verification_share *
|
||||||
|
self.scalar *
|
||||||
|
self.core.interpolation.interpolation_factor(*i, &included);
|
||||||
|
verification_shares.insert(*i, verification_share);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The offset is included by adding it to the participant with the lowest ID.
|
||||||
|
|
||||||
|
This is done after interpolating to ensure, regardless of the method of interpolation, that
|
||||||
|
the method of interpolation does not scale the offset. For Lagrange interpolation, we could
|
||||||
|
add the offset to every key share before interpolating, yet for Constant interpolation, we
|
||||||
|
_have_ to add it as we do here (which also works even when we intend to perform Lagrange
|
||||||
|
interpolation).
|
||||||
|
*/
|
||||||
|
if included[0] == self.params().i() {
|
||||||
|
*secret_share += self.offset;
|
||||||
|
}
|
||||||
|
*verification_shares.get_mut(&included[0]).unwrap() += C::generator() * self.offset;
|
||||||
|
|
||||||
|
Ok(ThresholdView {
|
||||||
|
interpolation: self.core.interpolation.clone(),
|
||||||
|
scalar: self.scalar,
|
||||||
|
offset: self.offset,
|
||||||
|
group_key: self.group_key(),
|
||||||
|
secret_share,
|
||||||
|
original_verification_shares: self.core.verification_shares.clone(),
|
||||||
|
verification_shares,
|
||||||
|
included,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write these keys to a type satisfying `std::io::Write`.
|
||||||
|
///
|
||||||
|
/// This will not include the ephemeral scalar/offset.
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
||||||
writer.write_all(&u32::try_from(C::ID.len()).unwrap().to_le_bytes())?;
|
writer.write_all(&u32::try_from(C::ID.len()).unwrap().to_le_bytes())?;
|
||||||
writer.write_all(C::ID)?;
|
writer.write_all(C::ID)?;
|
||||||
writer.write_all(&self.params.t.to_le_bytes())?;
|
writer.write_all(&self.core.params.t.to_le_bytes())?;
|
||||||
writer.write_all(&self.params.n.to_le_bytes())?;
|
writer.write_all(&self.core.params.n.to_le_bytes())?;
|
||||||
writer.write_all(&self.params.i.to_bytes())?;
|
writer.write_all(&self.core.params.i.to_bytes())?;
|
||||||
let mut share_bytes = self.secret_share.to_repr();
|
match &self.core.interpolation {
|
||||||
|
Interpolation::Constant(c) => {
|
||||||
|
writer.write_all(&[0])?;
|
||||||
|
for c in c {
|
||||||
|
writer.write_all(c.to_repr().as_ref())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Interpolation::Lagrange => writer.write_all(&[1])?,
|
||||||
|
};
|
||||||
|
let mut share_bytes = self.core.secret_share.to_repr();
|
||||||
writer.write_all(share_bytes.as_ref())?;
|
writer.write_all(share_bytes.as_ref())?;
|
||||||
share_bytes.as_mut().zeroize();
|
share_bytes.as_mut().zeroize();
|
||||||
for l in 1 ..= self.params.n {
|
for l in 1 ..= self.core.params.n {
|
||||||
writer
|
writer.write_all(
|
||||||
.write_all(self.verification_shares[&Participant::new(l).unwrap()].to_bytes().as_ref())?;
|
self.core.verification_shares[&Participant::new(l).unwrap()].to_bytes().as_ref(),
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize these keys to a `Vec<u8>`.
|
/// Serialize these keys to a `Vec<u8>`.
|
||||||
|
///
|
||||||
|
/// This will not include the ephemeral scalar/offset.
|
||||||
pub fn serialize(&self) -> Zeroizing<Vec<u8>> {
|
pub fn serialize(&self) -> Zeroizing<Vec<u8>> {
|
||||||
let mut serialized = Zeroizing::new(vec![]);
|
let mut serialized = Zeroizing::new(vec![]);
|
||||||
self.write::<Vec<u8>>(serialized.as_mut()).unwrap();
|
self.write::<Vec<u8>>(serialized.as_mut()).unwrap();
|
||||||
serialized
|
serialized
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read keys from a type satisfying std::io::Read.
|
/// Read keys from a type satisfying `std::io::Read`.
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<ThresholdCore<C>> {
|
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<ThresholdKeys<C>> {
|
||||||
{
|
{
|
||||||
let different = || io::Error::other("deserializing ThresholdCore for another curve");
|
let different = || io::Error::other("deserializing ThresholdKeys for another curve");
|
||||||
|
|
||||||
let mut id_len = [0; 4];
|
let mut id_len = [0; 4];
|
||||||
reader.read_exact(&mut id_len)?;
|
reader.read_exact(&mut id_len)?;
|
||||||
@@ -352,6 +599,20 @@ mod lib {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut interpolation = [0];
|
||||||
|
reader.read_exact(&mut interpolation)?;
|
||||||
|
let interpolation = match interpolation[0] {
|
||||||
|
0 => Interpolation::Constant({
|
||||||
|
let mut res = Vec::with_capacity(usize::from(n));
|
||||||
|
for _ in 0 .. n {
|
||||||
|
res.push(C::read_F(reader)?);
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}),
|
||||||
|
1 => Interpolation::Lagrange,
|
||||||
|
_ => Err(io::Error::other("invalid interpolation method"))?,
|
||||||
|
};
|
||||||
|
|
||||||
let secret_share = Zeroizing::new(C::read_F(reader)?);
|
let secret_share = Zeroizing::new(C::read_F(reader)?);
|
||||||
|
|
||||||
let mut verification_shares = HashMap::new();
|
let mut verification_shares = HashMap::new();
|
||||||
@@ -359,161 +620,23 @@ mod lib {
|
|||||||
verification_shares.insert(l, <C as Ciphersuite>::read_G(reader)?);
|
verification_shares.insert(l, <C as Ciphersuite>::read_G(reader)?);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ThresholdCore::new(
|
ThresholdKeys::new(
|
||||||
ThresholdParams::new(t, n, i).map_err(|_| io::Error::other("invalid parameters"))?,
|
ThresholdParams::new(t, n, i).map_err(io::Error::other)?,
|
||||||
|
interpolation,
|
||||||
secret_share,
|
secret_share,
|
||||||
verification_shares,
|
verification_shares,
|
||||||
))
|
)
|
||||||
|
.map_err(io::Error::other)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: Ciphersuite> ThresholdView<C> {
|
||||||
|
/// Return the scalar applied to this view.
|
||||||
|
pub fn scalar(&self) -> C::F {
|
||||||
|
self.scalar
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Threshold keys usable for signing.
|
/// Return the offset applied to this view.
|
||||||
#[derive(Clone, Debug, Zeroize)]
|
|
||||||
pub struct ThresholdKeys<C: Ciphersuite> {
|
|
||||||
// Core keys.
|
|
||||||
// If this is the last reference, the underlying keys will be dropped. When that happens, the
|
|
||||||
// private key present within it will be zeroed out (as it's within Zeroizing).
|
|
||||||
#[zeroize(skip)]
|
|
||||||
pub(crate) core: Arc<ThresholdCore<C>>,
|
|
||||||
|
|
||||||
// Offset applied to these keys.
|
|
||||||
pub(crate) offset: Option<C::F>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// View of keys, interpolated and offset for usage.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ThresholdView<C: Ciphersuite> {
|
|
||||||
offset: C::F,
|
|
||||||
group_key: C::G,
|
|
||||||
included: Vec<Participant>,
|
|
||||||
secret_share: Zeroizing<C::F>,
|
|
||||||
original_verification_shares: HashMap<Participant, C::G>,
|
|
||||||
verification_shares: HashMap<Participant, C::G>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> fmt::Debug for ThresholdView<C> {
|
|
||||||
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt
|
|
||||||
.debug_struct("ThresholdView")
|
|
||||||
.field("offset", &self.offset)
|
|
||||||
.field("group_key", &self.group_key)
|
|
||||||
.field("included", &self.included)
|
|
||||||
.field("original_verification_shares", &self.original_verification_shares)
|
|
||||||
.field("verification_shares", &self.verification_shares)
|
|
||||||
.finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Zeroize for ThresholdView<C> {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.offset.zeroize();
|
|
||||||
self.group_key.zeroize();
|
|
||||||
self.included.zeroize();
|
|
||||||
self.secret_share.zeroize();
|
|
||||||
for share in self.original_verification_shares.values_mut() {
|
|
||||||
share.zeroize();
|
|
||||||
}
|
|
||||||
for share in self.verification_shares.values_mut() {
|
|
||||||
share.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> ThresholdKeys<C> {
|
|
||||||
/// Create a new set of ThresholdKeys from a ThresholdCore.
|
|
||||||
pub fn new(core: ThresholdCore<C>) -> ThresholdKeys<C> {
|
|
||||||
ThresholdKeys { core: Arc::new(core), offset: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Offset the keys by a given scalar to allow for various account and privacy schemes.
|
|
||||||
///
|
|
||||||
/// This offset is ephemeral and will not be included when these keys are serialized. It also
|
|
||||||
/// accumulates, so calling offset multiple times will produce a offset of the offsets' sum.
|
|
||||||
#[must_use]
|
|
||||||
pub fn offset(&self, offset: C::F) -> ThresholdKeys<C> {
|
|
||||||
let mut res = self.clone();
|
|
||||||
// Carry any existing offset
|
|
||||||
// Enables schemes like Monero's subaddresses which have a per-subaddress offset and then a
|
|
||||||
// one-time-key offset
|
|
||||||
res.offset = Some(offset + res.offset.unwrap_or(C::F::ZERO));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the current offset in-use for these keys.
|
|
||||||
pub fn current_offset(&self) -> Option<C::F> {
|
|
||||||
self.offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the parameters for these keys.
|
|
||||||
pub fn params(&self) -> ThresholdParams {
|
|
||||||
self.core.params
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the secret share for these keys.
|
|
||||||
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
|
||||||
&self.core.secret_share
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the group key, with any offset applied.
|
|
||||||
pub fn group_key(&self) -> C::G {
|
|
||||||
self.core.group_key + (C::generator() * self.offset.unwrap_or(C::F::ZERO))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return all participants' verification shares without any offsetting.
|
|
||||||
pub(crate) fn verification_shares(&self) -> HashMap<Participant, C::G> {
|
|
||||||
self.core.verification_shares()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize these keys to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Zeroizing<Vec<u8>> {
|
|
||||||
self.core.serialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain a view of these keys, with any offset applied, interpolated for the specified signing
|
|
||||||
/// set.
|
|
||||||
pub fn view(&self, mut included: Vec<Participant>) -> Result<ThresholdView<C>, DkgError<()>> {
|
|
||||||
if (included.len() < self.params().t.into()) ||
|
|
||||||
(usize::from(self.params().n()) < included.len())
|
|
||||||
{
|
|
||||||
Err(DkgError::InvalidSigningSet)?;
|
|
||||||
}
|
|
||||||
included.sort();
|
|
||||||
|
|
||||||
let mut secret_share = Zeroizing::new(
|
|
||||||
lagrange::<C::F>(self.params().i(), &included) * self.secret_share().deref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut verification_shares = self.verification_shares();
|
|
||||||
for (i, share) in &mut verification_shares {
|
|
||||||
*share *= lagrange::<C::F>(*i, &included);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The offset is included by adding it to the participant with the lowest ID
|
|
||||||
let offset = self.offset.unwrap_or(C::F::ZERO);
|
|
||||||
if included[0] == self.params().i() {
|
|
||||||
*secret_share += offset;
|
|
||||||
}
|
|
||||||
*verification_shares.get_mut(&included[0]).unwrap() += C::generator() * offset;
|
|
||||||
|
|
||||||
Ok(ThresholdView {
|
|
||||||
offset,
|
|
||||||
group_key: self.group_key(),
|
|
||||||
secret_share,
|
|
||||||
original_verification_shares: self.verification_shares(),
|
|
||||||
verification_shares,
|
|
||||||
included,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> From<ThresholdCore<C>> for ThresholdKeys<C> {
|
|
||||||
fn from(keys: ThresholdCore<C>) -> ThresholdKeys<C> {
|
|
||||||
ThresholdKeys::new(keys)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> ThresholdView<C> {
|
|
||||||
/// Return the offset for this view.
|
|
||||||
pub fn offset(&self) -> C::F {
|
pub fn offset(&self) -> C::F {
|
||||||
self.offset
|
self.offset
|
||||||
}
|
}
|
||||||
@@ -528,21 +651,31 @@ mod lib {
|
|||||||
&self.included
|
&self.included
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the interpolated, offset secret share.
|
/// Return the interpolation factor for a signer.
|
||||||
|
pub fn interpolation_factor(&self, participant: Participant) -> Option<C::F> {
|
||||||
|
if !self.included.contains(&participant) {
|
||||||
|
None?
|
||||||
|
}
|
||||||
|
Some(self.interpolation.interpolation_factor(participant, &self.included))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the interpolated secret share, with the expected linear combination taken.
|
||||||
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
pub fn secret_share(&self) -> &Zeroizing<C::F> {
|
||||||
&self.secret_share
|
&self.secret_share
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the original verification share for the specified participant.
|
/// Return the original (untweaked) verification share for the specified participant.
|
||||||
|
///
|
||||||
|
/// This will panic if the participant index is invalid for these keys.
|
||||||
pub fn original_verification_share(&self, l: Participant) -> C::G {
|
pub fn original_verification_share(&self, l: Participant) -> C::G {
|
||||||
self.original_verification_shares[&l]
|
self.original_verification_shares[&l]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the interpolated, offset verification share for the specified participant.
|
/// Return the interpolated verification share, with the expected linear combination taken,
|
||||||
|
/// for the specified participant.
|
||||||
|
///
|
||||||
|
/// This will panic if the participant was not included in the signing set.
|
||||||
pub fn verification_share(&self, l: Participant) -> C::G {
|
pub fn verification_share(&self, l: Participant) -> C::G {
|
||||||
self.verification_shares[&l]
|
self.verification_shares[&l]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use lib::*;
|
|
||||||
|
|||||||
@@ -1,141 +0,0 @@
|
|||||||
#[cfg(feature = "std")]
|
|
||||||
use core::ops::Deref;
|
|
||||||
use std_shims::{vec, vec::Vec, collections::HashSet};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std_shims::collections::HashMap;
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use ciphersuite::group::ff::Field;
|
|
||||||
use ciphersuite::{
|
|
||||||
group::{Group, GroupEncoding},
|
|
||||||
Ciphersuite,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::DkgError;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use crate::{Participant, ThresholdParams, ThresholdCore, lagrange};
|
|
||||||
|
|
||||||
fn check_keys<C: Ciphersuite>(keys: &[C::G]) -> Result<u16, DkgError<()>> {
|
|
||||||
if keys.is_empty() {
|
|
||||||
Err(DkgError::InvalidSigningSet)?;
|
|
||||||
}
|
|
||||||
// Too many signers
|
|
||||||
let keys_len = u16::try_from(keys.len()).map_err(|_| DkgError::InvalidSigningSet)?;
|
|
||||||
|
|
||||||
// Duplicated public keys
|
|
||||||
if keys.iter().map(|key| key.to_bytes().as_ref().to_vec()).collect::<HashSet<_>>().len() !=
|
|
||||||
keys.len()
|
|
||||||
{
|
|
||||||
Err(DkgError::InvalidSigningSet)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(keys_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function panics if called with keys whose length exceed 2**16.
|
|
||||||
// This is fine since it's internal and all calls occur after calling check_keys, which does check
|
|
||||||
// the keys' length.
|
|
||||||
fn binding_factor_transcript<C: Ciphersuite>(
|
|
||||||
context: &[u8],
|
|
||||||
keys: &[C::G],
|
|
||||||
) -> Result<Vec<u8>, DkgError<()>> {
|
|
||||||
let mut transcript = vec![];
|
|
||||||
transcript.push(u8::try_from(context.len()).map_err(|_| DkgError::InvalidSigningSet)?);
|
|
||||||
transcript.extend(context);
|
|
||||||
transcript.extend(u16::try_from(keys.len()).unwrap().to_le_bytes());
|
|
||||||
for key in keys {
|
|
||||||
transcript.extend(key.to_bytes().as_ref());
|
|
||||||
}
|
|
||||||
Ok(transcript)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn binding_factor<C: Ciphersuite>(mut transcript: Vec<u8>, i: u16) -> C::F {
|
|
||||||
transcript.extend(i.to_le_bytes());
|
|
||||||
C::hash_to_F(b"musig", &transcript)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The group key resulting from using this library's MuSig key gen.
|
|
||||||
///
|
|
||||||
/// This function will return an error if the context is longer than 255 bytes.
|
|
||||||
///
|
|
||||||
/// Creating an aggregate key with a list containing duplicated public keys will return an error.
|
|
||||||
pub fn musig_key<C: Ciphersuite>(context: &[u8], keys: &[C::G]) -> Result<C::G, DkgError<()>> {
|
|
||||||
let keys_len = check_keys::<C>(keys)?;
|
|
||||||
let transcript = binding_factor_transcript::<C>(context, keys)?;
|
|
||||||
let mut res = C::G::identity();
|
|
||||||
for i in 1 ..= keys_len {
|
|
||||||
res += keys[usize::from(i - 1)] * binding_factor::<C>(transcript.clone(), i);
|
|
||||||
}
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A n-of-n non-interactive DKG which does not guarantee the usability of the resulting key.
|
|
||||||
///
|
|
||||||
/// Creating an aggregate key with a list containing duplicated public keys returns an error.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn musig<C: Ciphersuite>(
|
|
||||||
context: &[u8],
|
|
||||||
private_key: &Zeroizing<C::F>,
|
|
||||||
keys: &[C::G],
|
|
||||||
) -> Result<ThresholdCore<C>, DkgError<()>> {
|
|
||||||
let keys_len = check_keys::<C>(keys)?;
|
|
||||||
|
|
||||||
let our_pub_key = C::generator() * private_key.deref();
|
|
||||||
let Some(pos) = keys.iter().position(|key| *key == our_pub_key) else {
|
|
||||||
// Not present in signing set
|
|
||||||
Err(DkgError::InvalidSigningSet)?
|
|
||||||
};
|
|
||||||
let params = ThresholdParams::new(
|
|
||||||
keys_len,
|
|
||||||
keys_len,
|
|
||||||
// These errors shouldn't be possible, as pos is bounded to len - 1
|
|
||||||
// Since len is prior guaranteed to be within u16::MAX, pos + 1 must also be
|
|
||||||
Participant::new((pos + 1).try_into().map_err(|_| DkgError::InvalidSigningSet)?)
|
|
||||||
.ok_or(DkgError::InvalidSigningSet)?,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Calculate the binding factor per-key
|
|
||||||
let transcript = binding_factor_transcript::<C>(context, keys)?;
|
|
||||||
let mut binding = Vec::with_capacity(keys.len());
|
|
||||||
for i in 1 ..= keys_len {
|
|
||||||
binding.push(binding_factor::<C>(transcript.clone(), i));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multiply our private key by our binding factor
|
|
||||||
let mut secret_share = private_key.clone();
|
|
||||||
*secret_share *= binding[pos];
|
|
||||||
|
|
||||||
// Calculate verification shares
|
|
||||||
let mut verification_shares = HashMap::new();
|
|
||||||
// When this library offers a ThresholdView for a specific signing set, it applies the lagrange
|
|
||||||
// factor
|
|
||||||
// Since this is a n-of-n scheme, there's only one possible signing set, and one possible
|
|
||||||
// lagrange factor
|
|
||||||
// In the name of simplicity, we define the group key as the sum of all bound keys
|
|
||||||
// Accordingly, the secret share must be multiplied by the inverse of the lagrange factor, along
|
|
||||||
// with all verification shares
|
|
||||||
// This is less performant than simply defining the group key as the sum of all post-lagrange
|
|
||||||
// bound keys, yet the simplicity is preferred
|
|
||||||
let included = (1 ..= keys_len)
|
|
||||||
// This error also shouldn't be possible, for the same reasons as documented above
|
|
||||||
.map(|l| Participant::new(l).ok_or(DkgError::InvalidSigningSet))
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
let mut group_key = C::G::identity();
|
|
||||||
for (l, p) in included.iter().enumerate() {
|
|
||||||
let bound = keys[l] * binding[l];
|
|
||||||
group_key += bound;
|
|
||||||
|
|
||||||
let lagrange_inv = lagrange::<C::F>(*p, &included).invert().unwrap();
|
|
||||||
if params.i() == *p {
|
|
||||||
*secret_share *= lagrange_inv;
|
|
||||||
}
|
|
||||||
verification_shares.insert(*p, bound * lagrange_inv);
|
|
||||||
}
|
|
||||||
debug_assert_eq!(C::generator() * secret_share.deref(), verification_shares[¶ms.i()]);
|
|
||||||
debug_assert_eq!(musig_key::<C>(context, keys).unwrap(), group_key);
|
|
||||||
|
|
||||||
Ok(ThresholdCore { params, secret_share, group_key, verification_shares })
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
|
||||||
|
|
||||||
use crate::{Participant, ThresholdCore, ThresholdKeys, lagrange, musig::musig as musig_fn};
|
|
||||||
|
|
||||||
mod musig;
|
|
||||||
pub use musig::test_musig;
|
|
||||||
|
|
||||||
/// FROST key generation testing utility.
|
|
||||||
pub mod pedpop;
|
|
||||||
use pedpop::pedpop_gen;
|
|
||||||
|
|
||||||
// Promotion test.
|
|
||||||
mod promote;
|
|
||||||
use promote::test_generator_promotion;
|
|
||||||
|
|
||||||
/// Constant amount of participants to use when testing.
|
|
||||||
pub const PARTICIPANTS: u16 = 5;
|
|
||||||
/// Constant threshold of participants to use when testing.
|
|
||||||
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
|
||||||
|
|
||||||
/// Clone a map without a specific value.
|
|
||||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
|
||||||
map: &HashMap<K, V>,
|
|
||||||
without: &K,
|
|
||||||
) -> HashMap<K, V> {
|
|
||||||
let mut res = map.clone();
|
|
||||||
res.remove(without).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Recover the secret from a collection of keys.
|
|
||||||
///
|
|
||||||
/// This will panic if no keys, an insufficient amount of keys, or the wrong keys are provided.
|
|
||||||
pub fn recover_key<C: Ciphersuite>(keys: &HashMap<Participant, ThresholdKeys<C>>) -> C::F {
|
|
||||||
let first = keys.values().next().expect("no keys provided");
|
|
||||||
assert!(keys.len() >= first.params().t().into(), "not enough keys provided");
|
|
||||||
let included = keys.keys().copied().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let group_private = keys.iter().fold(C::F::ZERO, |accum, (i, keys)| {
|
|
||||||
accum + (lagrange::<C::F>(*i, &included) * keys.secret_share().deref())
|
|
||||||
});
|
|
||||||
assert_eq!(C::generator() * group_private, first.group_key(), "failed to recover keys");
|
|
||||||
group_private
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate threshold keys for tests.
|
|
||||||
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|
||||||
rng: &mut R,
|
|
||||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
|
||||||
let res = pedpop_gen(rng)
|
|
||||||
.drain()
|
|
||||||
.map(|(i, core)| {
|
|
||||||
assert_eq!(
|
|
||||||
&ThresholdCore::<C>::read::<&[u8]>(&mut core.serialize().as_ref()).unwrap(),
|
|
||||||
&core
|
|
||||||
);
|
|
||||||
(i, ThresholdKeys::new(core))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate MuSig keys for tests.
|
|
||||||
pub fn musig_key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|
||||||
rng: &mut R,
|
|
||||||
) -> HashMap<Participant, ThresholdKeys<C>> {
|
|
||||||
let mut keys = vec![];
|
|
||||||
let mut pub_keys = vec![];
|
|
||||||
for _ in 0 .. PARTICIPANTS {
|
|
||||||
let key = Zeroizing::new(C::F::random(&mut *rng));
|
|
||||||
pub_keys.push(C::generator() * *key);
|
|
||||||
keys.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = HashMap::new();
|
|
||||||
for key in keys {
|
|
||||||
let these_keys = musig_fn::<C>(b"Test MuSig Key Gen", &key, &pub_keys).unwrap();
|
|
||||||
res.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(C::generator() * recover_key(&res), res[&Participant(1)].group_key());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run the test suite on a ciphersuite.
|
|
||||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
|
||||||
key_gen::<_, C>(rng);
|
|
||||||
test_generator_promotion::<_, C>(rng);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_with_ristretto() {
|
|
||||||
test_ciphersuite::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng);
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use ciphersuite::{group::ff::Field, Ciphersuite};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ThresholdKeys,
|
|
||||||
musig::{musig_key, musig},
|
|
||||||
tests::{PARTICIPANTS, recover_key},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Tests MuSig key generation.
|
|
||||||
pub fn test_musig<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
|
||||||
let mut keys = vec![];
|
|
||||||
let mut pub_keys = vec![];
|
|
||||||
for _ in 0 .. PARTICIPANTS {
|
|
||||||
let key = Zeroizing::new(C::F::random(&mut *rng));
|
|
||||||
pub_keys.push(C::generator() * *key);
|
|
||||||
keys.push(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONTEXT: &[u8] = b"MuSig Test";
|
|
||||||
|
|
||||||
// Empty signing set
|
|
||||||
musig::<C>(CONTEXT, &Zeroizing::new(C::F::ZERO), &[]).unwrap_err();
|
|
||||||
// Signing set we're not part of
|
|
||||||
musig::<C>(CONTEXT, &Zeroizing::new(C::F::ZERO), &[C::generator()]).unwrap_err();
|
|
||||||
|
|
||||||
// Test with n keys
|
|
||||||
{
|
|
||||||
let mut created_keys = HashMap::new();
|
|
||||||
let mut verification_shares = HashMap::new();
|
|
||||||
let group_key = musig_key::<C>(CONTEXT, &pub_keys).unwrap();
|
|
||||||
for (i, key) in keys.iter().enumerate() {
|
|
||||||
let these_keys = musig::<C>(CONTEXT, key, &pub_keys).unwrap();
|
|
||||||
assert_eq!(these_keys.params().t(), PARTICIPANTS);
|
|
||||||
assert_eq!(these_keys.params().n(), PARTICIPANTS);
|
|
||||||
assert_eq!(usize::from(these_keys.params().i().0), i + 1);
|
|
||||||
|
|
||||||
verification_shares
|
|
||||||
.insert(these_keys.params().i(), C::generator() * **these_keys.secret_share());
|
|
||||||
|
|
||||||
assert_eq!(these_keys.group_key(), group_key);
|
|
||||||
|
|
||||||
created_keys.insert(these_keys.params().i(), ThresholdKeys::new(these_keys));
|
|
||||||
}
|
|
||||||
|
|
||||||
for keys in created_keys.values() {
|
|
||||||
assert_eq!(keys.verification_shares(), verification_shares);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(C::generator() * recover_key(&created_keys), group_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn musig_literal() {
|
|
||||||
test_musig::<_, ciphersuite::Ristretto>(&mut rand_core::OsRng)
|
|
||||||
}
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use ciphersuite::Ciphersuite;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Participant, ThresholdParams, ThresholdCore,
|
|
||||||
pedpop::{Commitments, KeyGenMachine, SecretShare, KeyMachine},
|
|
||||||
encryption::{EncryptionKeyMessage, EncryptedMessage},
|
|
||||||
tests::{THRESHOLD, PARTICIPANTS, clone_without},
|
|
||||||
};
|
|
||||||
|
|
||||||
type PedPoPEncryptedMessage<C> = EncryptedMessage<C, SecretShare<<C as Ciphersuite>::F>>;
|
|
||||||
type PedPoPSecretShares<C> = HashMap<Participant, PedPoPEncryptedMessage<C>>;
|
|
||||||
|
|
||||||
const CONTEXT: &str = "DKG Test Key Generation";
|
|
||||||
|
|
||||||
// Commit, then return commitment messages, enc keys, and shares
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
fn commit_enc_keys_and_shares<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|
||||||
rng: &mut R,
|
|
||||||
) -> (
|
|
||||||
HashMap<Participant, KeyMachine<C>>,
|
|
||||||
HashMap<Participant, EncryptionKeyMessage<C, Commitments<C>>>,
|
|
||||||
HashMap<Participant, C::G>,
|
|
||||||
HashMap<Participant, PedPoPSecretShares<C>>,
|
|
||||||
) {
|
|
||||||
let mut machines = HashMap::new();
|
|
||||||
let mut commitments = HashMap::new();
|
|
||||||
let mut enc_keys = HashMap::new();
|
|
||||||
for i in (1 ..= PARTICIPANTS).map(Participant) {
|
|
||||||
let params = ThresholdParams::new(THRESHOLD, PARTICIPANTS, i).unwrap();
|
|
||||||
let machine = KeyGenMachine::<C>::new(params, CONTEXT.to_string());
|
|
||||||
let (machine, these_commitments) = machine.generate_coefficients(rng);
|
|
||||||
machines.insert(i, machine);
|
|
||||||
|
|
||||||
commitments.insert(
|
|
||||||
i,
|
|
||||||
EncryptionKeyMessage::read::<&[u8]>(&mut these_commitments.serialize().as_ref(), params)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
enc_keys.insert(i, commitments[&i].enc_key());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut secret_shares = HashMap::new();
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.map(|(l, machine)| {
|
|
||||||
let (machine, mut shares) =
|
|
||||||
machine.generate_secret_shares(rng, clone_without(&commitments, &l)).unwrap();
|
|
||||||
let shares = shares
|
|
||||||
.drain()
|
|
||||||
.map(|(l, share)| {
|
|
||||||
(
|
|
||||||
l,
|
|
||||||
EncryptedMessage::read::<&[u8]>(
|
|
||||||
&mut share.serialize().as_ref(),
|
|
||||||
// Only t/n actually matters, so hardcode i to 1 here
|
|
||||||
ThresholdParams { t: THRESHOLD, n: PARTICIPANTS, i: Participant(1) },
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
secret_shares.insert(l, shares);
|
|
||||||
(l, machine)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>();
|
|
||||||
|
|
||||||
(machines, commitments, enc_keys, secret_shares)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_secret_shares<C: Ciphersuite>(
|
|
||||||
shares: &HashMap<Participant, PedPoPSecretShares<C>>,
|
|
||||||
recipient: Participant,
|
|
||||||
) -> PedPoPSecretShares<C> {
|
|
||||||
let mut our_secret_shares = HashMap::new();
|
|
||||||
for (i, shares) in shares {
|
|
||||||
if recipient == *i {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
our_secret_shares.insert(*i, shares[&recipient].clone());
|
|
||||||
}
|
|
||||||
our_secret_shares
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fully perform the PedPoP key generation algorithm.
|
|
||||||
pub fn pedpop_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
|
||||||
rng: &mut R,
|
|
||||||
) -> HashMap<Participant, ThresholdCore<C>> {
|
|
||||||
let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng);
|
|
||||||
|
|
||||||
let mut verification_shares = None;
|
|
||||||
let mut group_key = None;
|
|
||||||
machines
|
|
||||||
.drain()
|
|
||||||
.map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let these_keys = machine.calculate_share(rng, our_secret_shares).unwrap().complete();
|
|
||||||
|
|
||||||
// Verify the verification_shares are agreed upon
|
|
||||||
if verification_shares.is_none() {
|
|
||||||
verification_shares = Some(these_keys.verification_shares());
|
|
||||||
}
|
|
||||||
assert_eq!(verification_shares.as_ref().unwrap(), &these_keys.verification_shares());
|
|
||||||
|
|
||||||
// Verify the group keys are agreed upon
|
|
||||||
if group_key.is_none() {
|
|
||||||
group_key = Some(these_keys.group_key());
|
|
||||||
}
|
|
||||||
assert_eq!(group_key.unwrap(), these_keys.group_key());
|
|
||||||
|
|
||||||
(i, these_keys)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<_, _>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod literal {
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use ciphersuite::Ristretto;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
DkgError,
|
|
||||||
encryption::EncryptionKeyProof,
|
|
||||||
pedpop::{BlameMachine, AdditionalBlameMachine},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
const ONE: Participant = Participant(1);
|
|
||||||
const TWO: Participant = Participant(2);
|
|
||||||
|
|
||||||
fn test_blame(
|
|
||||||
commitment_msgs: &HashMap<Participant, EncryptionKeyMessage<Ristretto, Commitments<Ristretto>>>,
|
|
||||||
machines: Vec<BlameMachine<Ristretto>>,
|
|
||||||
msg: &PedPoPEncryptedMessage<Ristretto>,
|
|
||||||
blame: &Option<EncryptionKeyProof<Ristretto>>,
|
|
||||||
) {
|
|
||||||
for machine in machines {
|
|
||||||
let (additional, blamed) = machine.blame(ONE, TWO, msg.clone(), blame.clone());
|
|
||||||
assert_eq!(blamed, ONE);
|
|
||||||
// Verify additional blame also works
|
|
||||||
assert_eq!(additional.blame(ONE, TWO, msg.clone(), blame.clone()), ONE);
|
|
||||||
|
|
||||||
// Verify machines constructed with AdditionalBlameMachine::new work
|
|
||||||
assert_eq!(
|
|
||||||
AdditionalBlameMachine::new(
|
|
||||||
&mut OsRng,
|
|
||||||
CONTEXT.to_string(),
|
|
||||||
PARTICIPANTS,
|
|
||||||
commitment_msgs.clone()
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.blame(ONE, TWO, msg.clone(), blame.clone()),
|
|
||||||
ONE,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Write a macro which expands to the following
|
|
||||||
#[test]
|
|
||||||
fn invalid_encryption_pop_blame() {
|
|
||||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
// Mutate the PoP of the encrypted message from 1 to 2
|
|
||||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_pop();
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == TWO {
|
|
||||||
assert_eq!(machine.err(), Some(DkgError::InvalidShare { participant: ONE, blame: None }));
|
|
||||||
// Explicitly declare we have a blame object, which happens to be None since invalid PoP
|
|
||||||
// is self-explainable
|
|
||||||
blame = Some(None);
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_ecdh_blame() {
|
|
||||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
// Mutate the share to trigger a blame event
|
|
||||||
// Mutates from 2 to 1, as 1 is expected to end up malicious for test_blame to pass
|
|
||||||
// While here, 2 is malicious, this is so 1 creates the blame proof
|
|
||||||
// We then malleate 1's blame proof, so 1 ends up malicious
|
|
||||||
// Doesn't simply invalidate the PoP as that won't have a blame statement
|
|
||||||
// By mutating the encrypted data, we do ensure a blame statement is created
|
|
||||||
secret_shares
|
|
||||||
.get_mut(&TWO)
|
|
||||||
.unwrap()
|
|
||||||
.get_mut(&ONE)
|
|
||||||
.unwrap()
|
|
||||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == ONE {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_key();
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should be largely equivalent to the prior test
|
|
||||||
#[test]
|
|
||||||
fn invalid_dleq_blame() {
|
|
||||||
let (mut machines, commitment_msgs, _, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
secret_shares
|
|
||||||
.get_mut(&TWO)
|
|
||||||
.unwrap()
|
|
||||||
.get_mut(&ONE)
|
|
||||||
.unwrap()
|
|
||||||
.invalidate_msg(&mut OsRng, CONTEXT, TWO);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == ONE {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(DkgError::InvalidShare { participant: TWO, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
blame.as_mut().unwrap().as_mut().unwrap().invalidate_dleq();
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&TWO][&ONE].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_share_serialization_blame() {
|
|
||||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_serialization(
|
|
||||||
&mut OsRng,
|
|
||||||
CONTEXT,
|
|
||||||
ONE,
|
|
||||||
enc_keys[&TWO],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == TWO {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_share_value_blame() {
|
|
||||||
let (mut machines, commitment_msgs, enc_keys, mut secret_shares) =
|
|
||||||
commit_enc_keys_and_shares::<_, Ristretto>(&mut OsRng);
|
|
||||||
|
|
||||||
secret_shares.get_mut(&ONE).unwrap().get_mut(&TWO).unwrap().invalidate_share_value(
|
|
||||||
&mut OsRng,
|
|
||||||
CONTEXT,
|
|
||||||
ONE,
|
|
||||||
enc_keys[&TWO],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut blame = None;
|
|
||||||
let machines = machines
|
|
||||||
.drain()
|
|
||||||
.filter_map(|(i, machine)| {
|
|
||||||
let our_secret_shares = generate_secret_shares(&secret_shares, i);
|
|
||||||
let machine = machine.calculate_share(&mut OsRng, our_secret_shares);
|
|
||||||
if i == TWO {
|
|
||||||
blame = Some(match machine.err() {
|
|
||||||
Some(DkgError::InvalidShare { participant: ONE, blame: Some(blame) }) => Some(blame),
|
|
||||||
_ => panic!(),
|
|
||||||
});
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(machine.unwrap())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
test_blame(&commitment_msgs, machines, &secret_shares[&ONE][&TWO].clone(), &blame.unwrap());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
use core::{marker::PhantomData, ops::Deref};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use ciphersuite::{group::Group, Ciphersuite};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
promote::{GeneratorPromotion, GeneratorProof},
|
|
||||||
tests::{clone_without, key_gen, recover_key},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
struct AltGenerator<C: Ciphersuite> {
|
|
||||||
_curve: PhantomData<C>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Ciphersuite> Ciphersuite for AltGenerator<C> {
|
|
||||||
type F = C::F;
|
|
||||||
type G = C::G;
|
|
||||||
type H = C::H;
|
|
||||||
|
|
||||||
const ID: &'static [u8] = b"Alternate Ciphersuite";
|
|
||||||
|
|
||||||
fn generator() -> Self::G {
|
|
||||||
C::G::generator() * <C as Ciphersuite>::hash_to_F(b"DKG Promotion Test", b"generator")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hash_to_F(dst: &[u8], data: &[u8]) -> Self::F {
|
|
||||||
<C as Ciphersuite>::hash_to_F(dst, data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test promotion of threshold keys to another generator
|
|
||||||
pub(crate) fn test_generator_promotion<R: RngCore + CryptoRng, C: Ciphersuite>(rng: &mut R) {
|
|
||||||
let keys = key_gen::<_, C>(&mut *rng);
|
|
||||||
|
|
||||||
let mut promotions = HashMap::new();
|
|
||||||
let mut proofs = HashMap::new();
|
|
||||||
for (i, keys) in &keys {
|
|
||||||
let (promotion, proof) =
|
|
||||||
GeneratorPromotion::<_, AltGenerator<C>>::promote(&mut *rng, keys.clone());
|
|
||||||
promotions.insert(*i, promotion);
|
|
||||||
proofs.insert(*i, GeneratorProof::<C>::read::<&[u8]>(&mut proof.serialize().as_ref()).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_group_key = AltGenerator::<C>::generator() * recover_key(&keys);
|
|
||||||
for (i, promoting) in promotions.drain() {
|
|
||||||
let promoted = promoting.complete(&clone_without(&proofs, &i)).unwrap();
|
|
||||||
assert_eq!(keys[&i].params(), promoted.params());
|
|
||||||
assert_eq!(keys[&i].secret_share(), promoted.secret_share());
|
|
||||||
assert_eq!(new_group_key, promoted.group_key());
|
|
||||||
for (l, verification_share) in promoted.verification_shares() {
|
|
||||||
assert_eq!(
|
|
||||||
AltGenerator::<C>::generator() * keys[&l].secret_share().deref(),
|
|
||||||
verification_share
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,7 @@ workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
rustversion = "1"
|
rustversion = "1"
|
||||||
|
|
||||||
thiserror = { version = "1", optional = true }
|
thiserror = { version = "2", default-features = false, optional = true }
|
||||||
rand_core = { version = "0.6", default-features = false }
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
||||||
@@ -44,7 +44,7 @@ dalek-ff-group = { path = "../dalek-ff-group" }
|
|||||||
transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] }
|
transcript = { package = "flexible-transcript", path = "../transcript", features = ["recommended"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = ["rand_core/std", "zeroize/std", "digest/std", "transcript/std", "ff/std", "multiexp?/std"]
|
std = ["thiserror?/std", "rand_core/std", "zeroize/std", "digest/std", "transcript/std", "ff/std", "multiexp?/std"]
|
||||||
serialize = ["std"]
|
serialize = ["std"]
|
||||||
|
|
||||||
# Needed for cross-group DLEqs
|
# Needed for cross-group DLEqs
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ impl<G: PrimeGroup> Generators<G> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Error for cross-group DLEq proofs.
|
/// Error for cross-group DLEq proofs.
|
||||||
#[derive(Error, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum DLEqError {
|
pub enum DLEqError {
|
||||||
/// Invalid proof length.
|
/// Invalid proof length.
|
||||||
#[error("invalid proof length")]
|
#[error("invalid proof length")]
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ pub(crate) fn challenge<T: Transcript, F: PrimeField>(transcript: &mut T) -> F {
|
|||||||
// Get a wide amount of bytes to safely reduce without bias
|
// Get a wide amount of bytes to safely reduce without bias
|
||||||
// In most cases, <=1.5x bytes is enough. 2x is still standard and there's some theoretical
|
// In most cases, <=1.5x bytes is enough. 2x is still standard and there's some theoretical
|
||||||
// groups which may technically require more than 1.5x bytes for this to work as intended
|
// groups which may technically require more than 1.5x bytes for this to work as intended
|
||||||
let target_bytes = ((usize::try_from(F::NUM_BITS).unwrap() + 7) / 8) * 2;
|
let target_bytes = usize::try_from(F::NUM_BITS).unwrap().div_ceil(8) * 2;
|
||||||
let mut challenge_bytes = transcript.challenge(b"challenge");
|
let mut challenge_bytes = transcript.challenge(b"challenge");
|
||||||
let challenge_bytes_len = challenge_bytes.as_ref().len();
|
let challenge_bytes_len = challenge_bytes.as_ref().len();
|
||||||
// If the challenge is 32 bytes, and we need 64, we need two challenges
|
// If the challenge is 32 bytes, and we need 64, we need two challenges
|
||||||
let needed_challenges = (target_bytes + (challenge_bytes_len - 1)) / challenge_bytes_len;
|
let needed_challenges = target_bytes.div_ceil(challenge_bytes_len);
|
||||||
|
|
||||||
// The following algorithm should be equivalent to a wide reduction of the challenges,
|
// The following algorithm should be equivalent to a wide reduction of the challenges,
|
||||||
// interpreted as concatenated, big-endian byte string
|
// interpreted as concatenated, big-endian byte string
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "minimal-ed448"
|
name = "minimal-ed448"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
description = "Unaudited, inefficient implementation of Ed448 in Rust"
|
description = "Unaudited, inefficient implementation of Ed448 in Rust"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ed448"
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ed448"
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["ed448", "ff", "group"]
|
keywords = ["ed448", "ff", "group"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.66"
|
rust-version = "1.65"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ use zeroize::Zeroize;
|
|||||||
|
|
||||||
// Use black_box when possible
|
// Use black_box when possible
|
||||||
#[rustversion::since(1.66)]
|
#[rustversion::since(1.66)]
|
||||||
use core::hint::black_box;
|
mod black_box {
|
||||||
#[rustversion::before(1.66)]
|
pub(crate) fn black_box<T>(val: T) -> T {
|
||||||
fn black_box<T>(val: T) -> T {
|
#[allow(clippy::incompatible_msrv)]
|
||||||
val
|
core::hint::black_box(val)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#[rustversion::before(1.66)]
|
||||||
|
mod black_box {
|
||||||
|
pub(crate) fn black_box<T>(val: T) -> T {
|
||||||
|
val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use black_box::black_box;
|
||||||
|
|
||||||
pub(crate) fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
pub(crate) fn u8_from_bool(bit_ref: &mut bool) -> u8 {
|
||||||
let bit_ref = black_box(bit_ref);
|
let bit_ref = black_box(bit_ref);
|
||||||
|
|||||||
@@ -154,18 +154,20 @@ pub fn test_group<R: RngCore, G: Group>(rng: &mut R) {
|
|||||||
|
|
||||||
/// Test encoding and decoding of group elements.
|
/// Test encoding and decoding of group elements.
|
||||||
pub fn test_encoding<G: PrimeGroup>() {
|
pub fn test_encoding<G: PrimeGroup>() {
|
||||||
let test = |point: G, msg| {
|
let test = |point: G, msg| -> G {
|
||||||
let bytes = point.to_bytes();
|
let bytes = point.to_bytes();
|
||||||
let mut repr = G::Repr::default();
|
let mut repr = G::Repr::default();
|
||||||
repr.as_mut().copy_from_slice(bytes.as_ref());
|
repr.as_mut().copy_from_slice(bytes.as_ref());
|
||||||
assert_eq!(point, G::from_bytes(&repr).unwrap(), "{msg} couldn't be encoded and decoded");
|
let decoded = G::from_bytes(&repr).unwrap();
|
||||||
|
assert_eq!(point, decoded, "{msg} couldn't be encoded and decoded");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
point,
|
point,
|
||||||
G::from_bytes_unchecked(&repr).unwrap(),
|
G::from_bytes_unchecked(&repr).unwrap(),
|
||||||
"{msg} couldn't be encoded and decoded",
|
"{msg} couldn't be encoded and decoded",
|
||||||
);
|
);
|
||||||
|
decoded
|
||||||
};
|
};
|
||||||
test(G::identity(), "identity");
|
assert!(bool::from(test(G::identity(), "identity").is_identity()));
|
||||||
test(G::generator(), "generator");
|
test(G::generator(), "generator");
|
||||||
test(G::generator() + G::generator(), "(generator * 2)");
|
test(G::generator() + G::generator(), "(generator * 2)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "modular-frost"
|
name = "modular-frost"
|
||||||
version = "0.9.0"
|
version = "0.10.1"
|
||||||
description = "Modular implementation of FROST over ff/group"
|
description = "Modular implementation of FROST over ff/group"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost"
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost"
|
||||||
@@ -17,7 +17,7 @@ rustdoc-args = ["--cfg", "docsrs"]
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1"
|
thiserror = { version = "2", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||||
rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
|
rand_chacha = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
@@ -39,13 +39,17 @@ multiexp = { path = "../multiexp", version = "0.4", default-features = false, fe
|
|||||||
|
|
||||||
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1", default-features = false, features = ["std"] }
|
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
dkg = { path = "../dkg", version = "^0.5.1", default-features = false, features = ["std"] }
|
dkg = { path = "../dkg", version = "0.6.1", default-features = false, features = ["std"] }
|
||||||
|
dkg-recovery = { path = "../dkg/recovery", version = "0.6", default-features = false, features = ["std"], optional = true }
|
||||||
|
dkg-dealer = { path = "../dkg/dealer", version = "0.6", default-features = false, features = ["std"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
dkg = { path = "../dkg", features = ["tests"] }
|
dkg = { path = "../dkg", default-features = false, features = ["std"] }
|
||||||
|
dkg-recovery = { path = "../dkg/recovery", default-features = false, features = ["std"] }
|
||||||
|
dkg-dealer = { path = "../dkg/dealer", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
ed25519 = ["dalek-ff-group", "ciphersuite/ed25519"]
|
ed25519 = ["dalek-ff-group", "ciphersuite/ed25519"]
|
||||||
@@ -56,4 +60,4 @@ p256 = ["ciphersuite/p256"]
|
|||||||
|
|
||||||
ed448 = ["minimal-ed448", "ciphersuite/ed448"]
|
ed448 = ["minimal-ed448", "ciphersuite/ed448"]
|
||||||
|
|
||||||
tests = ["hex", "rand_core/getrandom", "dkg/tests"]
|
tests = ["hex", "rand_core/getrandom", "dkg-dealer" ,"dkg-recovery"]
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ This library offers ciphersuites compatible with the
|
|||||||
[IETF draft](https://github.com/cfrg/draft-irtf-cfrg-frost). Currently, version
|
[IETF draft](https://github.com/cfrg/draft-irtf-cfrg-frost). Currently, version
|
||||||
15 is supported.
|
15 is supported.
|
||||||
|
|
||||||
|
A variety of testing utilities are provided under the `tests` feature. These
|
||||||
|
are provided with no guarantees and may have completely arbitrary behavior,
|
||||||
|
including panicking for completely well-reasoned input.
|
||||||
|
|
||||||
This library was
|
This library was
|
||||||
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
[audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf),
|
||||||
culminating in commit
|
culminating in commit
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ pub trait Hram<C: Curve>: Send + Sync + Clone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Schnorr signature algorithm ((R, s) where s = r + cx).
|
/// Schnorr signature algorithm ((R, s) where s = r + cx).
|
||||||
|
///
|
||||||
|
/// `verify`, `verify_share` must be called after `sign_share` is called.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Schnorr<C: Curve, T: Sync + Clone + Debug + Transcript, H: Hram<C>> {
|
pub struct Schnorr<C: Curve, T: Sync + Clone + Debug + Transcript, H: Hram<C>> {
|
||||||
transcript: T,
|
transcript: T,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use std::collections::HashMap;
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Distributed key generation protocol.
|
/// Distributed key generation protocol.
|
||||||
pub use dkg::{self, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, ThresholdView};
|
pub use dkg::{self, Participant, ThresholdParams, ThresholdKeys, ThresholdView};
|
||||||
|
|
||||||
/// Curve trait and provided curves/HRAMs, forming various ciphersuites.
|
/// Curve trait and provided curves/HRAMs, forming various ciphersuites.
|
||||||
pub mod curve;
|
pub mod curve;
|
||||||
|
|||||||
@@ -125,8 +125,11 @@ impl<C: Curve, A: Algorithm<C>> AlgorithmMachine<C, A> {
|
|||||||
let mut params = self.params;
|
let mut params = self.params;
|
||||||
|
|
||||||
let mut rng = ChaCha20Rng::from_seed(*seed.0);
|
let mut rng = ChaCha20Rng::from_seed(*seed.0);
|
||||||
let (nonces, commitments) =
|
let (nonces, commitments) = Commitments::new::<_>(
|
||||||
Commitments::new::<_>(&mut rng, params.keys.secret_share(), ¶ms.algorithm.nonces());
|
&mut rng,
|
||||||
|
params.keys.original_secret_share(),
|
||||||
|
¶ms.algorithm.nonces(),
|
||||||
|
);
|
||||||
let addendum = params.algorithm.preprocess_addendum(&mut rng, ¶ms.keys);
|
let addendum = params.algorithm.preprocess_addendum(&mut rng, ¶ms.keys);
|
||||||
|
|
||||||
let preprocess = Preprocess { commitments, addendum };
|
let preprocess = Preprocess { commitments, addendum };
|
||||||
@@ -203,14 +206,15 @@ pub trait SignMachine<S>: Send + Sync + Sized {
|
|||||||
/// SignatureMachine this SignMachine turns into.
|
/// SignatureMachine this SignMachine turns into.
|
||||||
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
|
type SignatureMachine: SignatureMachine<S, SignatureShare = Self::SignatureShare>;
|
||||||
|
|
||||||
/// Cache this preprocess for usage later. This cached preprocess MUST only be used once. Reuse
|
/// Cache this preprocess for usage later.
|
||||||
/// of it enables recovery of your private key share. Third-party recovery of a cached preprocess
|
///
|
||||||
/// also enables recovery of your private key share, so this MUST be treated with the same
|
/// This cached preprocess MUST only be used once. Reuse of it enables recovery of your private
|
||||||
/// security as your private key share.
|
/// key share. Third-party recovery of a cached preprocess also enables recovery of your private
|
||||||
|
/// key share, so this MUST be treated with the same security as your private key share.
|
||||||
fn cache(self) -> CachedPreprocess;
|
fn cache(self) -> CachedPreprocess;
|
||||||
|
|
||||||
/// Create a sign machine from a cached preprocess.
|
/// Create a sign machine from a cached preprocess.
|
||||||
|
///
|
||||||
/// After this, the preprocess must be deleted so it's never reused. Any reuse will presumably
|
/// After this, the preprocess must be deleted so it's never reused. Any reuse will presumably
|
||||||
/// cause the signer to leak their secret share.
|
/// cause the signer to leak their secret share.
|
||||||
fn from_cache(
|
fn from_cache(
|
||||||
@@ -219,11 +223,14 @@ pub trait SignMachine<S>: Send + Sync + Sized {
|
|||||||
cache: CachedPreprocess,
|
cache: CachedPreprocess,
|
||||||
) -> (Self, Self::Preprocess);
|
) -> (Self, Self::Preprocess);
|
||||||
|
|
||||||
/// Read a Preprocess message. Despite taking self, this does not save the preprocess.
|
/// Read a Preprocess message.
|
||||||
/// It must be externally cached and passed into sign.
|
///
|
||||||
|
/// Despite taking self, this does not save the preprocess. It must be externally cached and
|
||||||
|
/// passed into sign.
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
|
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess>;
|
||||||
|
|
||||||
/// Sign a message.
|
/// Sign a message.
|
||||||
|
///
|
||||||
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
|
/// Takes in the participants' preprocess messages. Returns the signature share to be broadcast
|
||||||
/// to all participants, over an authenticated channel. The parties who participate here will
|
/// to all participants, over an authenticated channel. The parties who participate here will
|
||||||
/// become the signing set for this session.
|
/// become the signing set for this session.
|
||||||
@@ -353,12 +360,7 @@ impl<C: Curve, A: Algorithm<C>> SignMachine<A::Signature> for AlgorithmSignMachi
|
|||||||
|
|
||||||
// Re-format into the FROST-expected rho transcript
|
// Re-format into the FROST-expected rho transcript
|
||||||
let mut rho_transcript = A::Transcript::new(b"FROST_rho");
|
let mut rho_transcript = A::Transcript::new(b"FROST_rho");
|
||||||
rho_transcript.append_message(
|
rho_transcript.append_message(b"group_key", self.params.keys.group_key().to_bytes());
|
||||||
b"group_key",
|
|
||||||
(self.params.keys.group_key() +
|
|
||||||
(C::generator() * self.params.keys.current_offset().unwrap_or(C::F::ZERO)))
|
|
||||||
.to_bytes(),
|
|
||||||
);
|
|
||||||
rho_transcript.append_message(b"message", C::hash_msg(msg));
|
rho_transcript.append_message(b"message", C::hash_msg(msg));
|
||||||
rho_transcript.append_message(
|
rho_transcript.append_message(
|
||||||
b"preprocesses",
|
b"preprocesses",
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
pub use dkg::tests::{key_gen, musig_key_gen, recover_key};
|
use ciphersuite::Ciphersuite;
|
||||||
|
pub use dkg_recovery::recover_key;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Curve, Participant, ThresholdKeys, FrostError,
|
Curve, Participant, ThresholdKeys, FrostError,
|
||||||
@@ -26,6 +27,18 @@ pub const PARTICIPANTS: u16 = 5;
|
|||||||
/// Constant threshold of participants to use when signing.
|
/// Constant threshold of participants to use when signing.
|
||||||
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
pub const THRESHOLD: u16 = ((PARTICIPANTS * 2) / 3) + 1;
|
||||||
|
|
||||||
|
/// Create a key, for testing purposes.
|
||||||
|
pub fn key_gen<R: RngCore + CryptoRng, C: Ciphersuite>(
|
||||||
|
rng: &mut R,
|
||||||
|
) -> HashMap<Participant, ThresholdKeys<C>> {
|
||||||
|
let res = dkg_dealer::key_gen::<R, C>(rng, THRESHOLD, PARTICIPANTS).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
C::generator() * *recover_key(&res.values().cloned().collect::<Vec<_>>()).unwrap(),
|
||||||
|
res.values().next().unwrap().group_key()
|
||||||
|
);
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
/// Clone a map without a specific value.
|
/// Clone a map without a specific value.
|
||||||
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
pub fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||||
map: &HashMap<K, V>,
|
map: &HashMap<K, V>,
|
||||||
@@ -238,12 +251,6 @@ pub fn test_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
|||||||
test_schnorr_with_keys::<_, _, H>(&mut *rng, &keys)
|
test_schnorr_with_keys::<_, _, H>(&mut *rng, &keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test a basic Schnorr signature, yet with MuSig.
|
|
||||||
pub fn test_musig_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
|
||||||
let keys = musig_key_gen(&mut *rng);
|
|
||||||
test_schnorr_with_keys::<_, _, H>(&mut *rng, &keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test an offset Schnorr signature.
|
/// Test an offset Schnorr signature.
|
||||||
pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||||
const MSG: &[u8] = b"Hello, World!";
|
const MSG: &[u8] = b"Hello, World!";
|
||||||
@@ -251,10 +258,11 @@ pub fn test_offset_schnorr<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &m
|
|||||||
let mut keys = key_gen(&mut *rng);
|
let mut keys = key_gen(&mut *rng);
|
||||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
||||||
|
|
||||||
|
let scalar = C::F::from(3);
|
||||||
let offset = C::F::from(5);
|
let offset = C::F::from(5);
|
||||||
let offset_key = group_key + (C::generator() * offset);
|
let offset_key = (group_key * scalar) + (C::generator() * offset);
|
||||||
for keys in keys.values_mut() {
|
for keys in keys.values_mut() {
|
||||||
*keys = keys.offset(offset);
|
*keys = keys.clone().scale(scalar).unwrap().offset(offset);
|
||||||
assert_eq!(keys.group_key(), offset_key);
|
assert_eq!(keys.group_key(), offset_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +297,6 @@ pub fn test_schnorr_blame<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mu
|
|||||||
/// Run a variety of tests against a ciphersuite.
|
/// Run a variety of tests against a ciphersuite.
|
||||||
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
pub fn test_ciphersuite<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(rng: &mut R) {
|
||||||
test_schnorr::<R, C, H>(rng);
|
test_schnorr::<R, C, H>(rng);
|
||||||
test_musig_schnorr::<R, C, H>(rng);
|
|
||||||
test_offset_schnorr::<R, C, H>(rng);
|
test_offset_schnorr::<R, C, H>(rng);
|
||||||
test_schnorr_blame::<R, C, H>(rng);
|
test_schnorr_blame::<R, C, H>(rng);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,10 @@ use transcript::{Transcript, RecommendedTranscript};
|
|||||||
|
|
||||||
use ciphersuite::group::{ff::Field, Group, GroupEncoding};
|
use ciphersuite::group::{ff::Field, Group, GroupEncoding};
|
||||||
|
|
||||||
pub use dkg::tests::{key_gen, recover_key};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Curve, Participant, ThresholdView, ThresholdKeys, FrostError,
|
Curve, Participant, ThresholdView, ThresholdKeys, FrostError,
|
||||||
algorithm::Algorithm,
|
algorithm::Algorithm,
|
||||||
tests::{algorithm_machines, sign},
|
tests::{key_gen, algorithm_machines, sign},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use ciphersuite::group::{ff::PrimeField, GroupEncoding};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
curve::Curve,
|
curve::Curve,
|
||||||
Participant, ThresholdCore, ThresholdKeys,
|
Participant, ThresholdKeys,
|
||||||
algorithm::{Hram, IetfSchnorr},
|
algorithm::{Hram, IetfSchnorr},
|
||||||
sign::{
|
sign::{
|
||||||
Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess,
|
Writable, Nonce, GeneratorCommitments, NonceCommitments, Commitments, Preprocess,
|
||||||
@@ -115,26 +115,27 @@ fn vectors_to_multisig_keys<C: Curve>(vectors: &Vectors) -> HashMap<Participant,
|
|||||||
|
|
||||||
let mut keys = HashMap::new();
|
let mut keys = HashMap::new();
|
||||||
for i in 1 ..= u16::try_from(shares.len()).unwrap() {
|
for i in 1 ..= u16::try_from(shares.len()).unwrap() {
|
||||||
// Manually re-implement the serialization for ThresholdCore to import this data
|
// Manually re-implement the serialization for ThresholdKeys to import this data
|
||||||
let mut serialized = vec![];
|
let mut serialized = vec![];
|
||||||
serialized.extend(u32::try_from(C::ID.len()).unwrap().to_le_bytes());
|
serialized.extend(u32::try_from(C::ID.len()).unwrap().to_le_bytes());
|
||||||
serialized.extend(C::ID);
|
serialized.extend(C::ID);
|
||||||
serialized.extend(vectors.threshold.to_le_bytes());
|
serialized.extend(vectors.threshold.to_le_bytes());
|
||||||
serialized.extend(u16::try_from(shares.len()).unwrap().to_le_bytes());
|
serialized.extend(u16::try_from(shares.len()).unwrap().to_le_bytes());
|
||||||
serialized.extend(i.to_le_bytes());
|
serialized.extend(i.to_le_bytes());
|
||||||
|
serialized.push(1);
|
||||||
serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref());
|
serialized.extend(shares[usize::from(i) - 1].to_repr().as_ref());
|
||||||
for share in &verification_shares {
|
for share in &verification_shares {
|
||||||
serialized.extend(share.to_bytes().as_ref());
|
serialized.extend(share.to_bytes().as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
let these_keys = ThresholdCore::<C>::read::<&[u8]>(&mut serialized.as_ref()).unwrap();
|
let these_keys = ThresholdKeys::<C>::read::<&[u8]>(&mut serialized.as_ref()).unwrap();
|
||||||
assert_eq!(these_keys.params().t(), vectors.threshold);
|
assert_eq!(these_keys.params().t(), vectors.threshold);
|
||||||
assert_eq!(usize::from(these_keys.params().n()), shares.len());
|
assert_eq!(usize::from(these_keys.params().n()), shares.len());
|
||||||
let participant = Participant::new(i).unwrap();
|
let participant = Participant::new(i).unwrap();
|
||||||
assert_eq!(these_keys.params().i(), participant);
|
assert_eq!(these_keys.params().i(), participant);
|
||||||
assert_eq!(these_keys.secret_share().deref(), &shares[usize::from(i - 1)]);
|
assert_eq!(these_keys.original_secret_share().deref(), &shares[usize::from(i - 1)]);
|
||||||
assert_eq!(hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key);
|
assert_eq!(hex::encode(these_keys.group_key().to_bytes().as_ref()), vectors.group_key);
|
||||||
keys.insert(participant, ThresholdKeys::new(these_keys));
|
keys.insert(participant, these_keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
keys
|
keys
|
||||||
@@ -156,7 +157,7 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
let secret =
|
let secret =
|
||||||
C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
|
C::read_F::<&[u8]>(&mut hex::decode(&vectors.group_secret).unwrap().as_ref()).unwrap();
|
||||||
assert_eq!(C::generator() * secret, group_key);
|
assert_eq!(C::generator() * secret, group_key);
|
||||||
assert_eq!(recover_key(&keys), secret);
|
assert_eq!(*recover_key(&keys.values().cloned().collect::<Vec<_>>()).unwrap(), secret);
|
||||||
|
|
||||||
let mut machines = vec![];
|
let mut machines = vec![];
|
||||||
for i in &vectors.included {
|
for i in &vectors.included {
|
||||||
@@ -345,13 +346,20 @@ pub fn test_with_vectors<R: RngCore + CryptoRng, C: Curve, H: Hram<C>>(
|
|||||||
|
|
||||||
// Calculate the expected nonces
|
// Calculate the expected nonces
|
||||||
let mut expected = (C::generator() *
|
let mut expected = (C::generator() *
|
||||||
C::random_nonce(keys[i].secret_share(), &mut TransparentRng(vec![randomness.0])).deref())
|
C::random_nonce(
|
||||||
|
keys[i].original_secret_share(),
|
||||||
|
&mut TransparentRng(vec![randomness.0]),
|
||||||
|
)
|
||||||
|
.deref())
|
||||||
.to_bytes()
|
.to_bytes()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
expected.extend(
|
expected.extend(
|
||||||
(C::generator() *
|
(C::generator() *
|
||||||
C::random_nonce(keys[i].secret_share(), &mut TransparentRng(vec![randomness.1]))
|
C::random_nonce(
|
||||||
|
keys[i].original_secret_share(),
|
||||||
|
&mut TransparentRng(vec![randomness.1]),
|
||||||
|
)
|
||||||
.deref())
|
.deref())
|
||||||
.to_bytes()
|
.to_bytes()
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/multiexp"
|
|||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["multiexp", "ff", "group"]
|
keywords = ["multiexp", "ff", "group"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.79"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub(crate) fn prep_bits<G: Group<Scalar: PrimeFieldBits>>(
|
|||||||
for pair in pairs {
|
for pair in pairs {
|
||||||
let p = groupings.len();
|
let p = groupings.len();
|
||||||
let mut bits = pair.0.to_le_bits();
|
let mut bits = pair.0.to_le_bits();
|
||||||
groupings.push(vec![0; (bits.len() + (w_usize - 1)) / w_usize]);
|
groupings.push(vec![0; bits.len().div_ceil(w_usize)]);
|
||||||
|
|
||||||
for (i, mut bit) in bits.iter_mut().enumerate() {
|
for (i, mut bit) in bits.iter_mut().enumerate() {
|
||||||
let mut bit = u8_from_bool(&mut bit);
|
let mut bit = u8_from_bool(&mut bit);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorr"
|
|||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["schnorr", "ff", "group"]
|
keywords = ["schnorr", "ff", "group"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.79"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -31,9 +31,8 @@ fn weight<D: Send + Clone + SecureDigest, F: PrimeField>(digest: &mut DigestTran
|
|||||||
// Derive a scalar from enough bits of entropy that bias is < 2^128
|
// Derive a scalar from enough bits of entropy that bias is < 2^128
|
||||||
// This can't be const due to its usage of a generic
|
// This can't be const due to its usage of a generic
|
||||||
// Also due to the usize::try_from, yet that could be replaced with an `as`
|
// Also due to the usize::try_from, yet that could be replaced with an `as`
|
||||||
// The + 7 forces it to round up
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
let BYTES: usize = usize::try_from(((F::NUM_BITS + 128) + 7) / 8).unwrap();
|
let BYTES: usize = usize::try_from((F::NUM_BITS + 128).div_ceil(8)).unwrap();
|
||||||
|
|
||||||
let mut remaining = BYTES;
|
let mut remaining = BYTES;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "frost-schnorrkel"
|
name = "frost-schnorrkel"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
description = "modular-frost Algorithm compatible with Schnorrkel"
|
description = "modular-frost Algorithm compatible with Schnorrkel"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorrkel"
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorrkel"
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["frost", "multisig", "threshold", "schnorrkel"]
|
keywords = ["frost", "multisig", "threshold", "schnorrkel"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.79"
|
rust-version = "1.80"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
@@ -26,7 +26,7 @@ group = "0.13"
|
|||||||
|
|
||||||
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", features = ["std", "ristretto"] }
|
ciphersuite = { path = "../ciphersuite", version = "^0.4.1", features = ["std", "ristretto"] }
|
||||||
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1" }
|
schnorr = { package = "schnorr-signatures", path = "../schnorr", version = "^0.5.1" }
|
||||||
frost = { path = "../frost", package = "modular-frost", version = "^0.9.0", features = ["ristretto"] }
|
frost = { path = "../frost", package = "modular-frost", version = "^0.10.0", features = ["ristretto"] }
|
||||||
|
|
||||||
schnorrkel = { version = "0.11" }
|
schnorrkel = { version = "0.11" }
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "flexible-transcript"
|
name = "flexible-transcript"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
description = "A simple transcript trait definition, along with viable options"
|
description = "A simple transcript trait definition, along with viable options"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/transcript"
|
repository = "https://github.com/serai-dex/serai/tree/develop/crypto/transcript"
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
keywords = ["transcript"]
|
keywords = ["transcript"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.79"
|
rust-version = "1.73"
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bitcoin-serai"
|
name = "bitcoin-serai"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "A Bitcoin library for FROST-signing transactions"
|
description = "A Bitcoin library for FROST-signing transactions"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/networks/bitcoin"
|
repository = "https://github.com/serai-dex/serai/tree/develop/networks/bitcoin"
|
||||||
@@ -20,13 +20,14 @@ std-shims = { version = "0.1.1", path = "../../common/std-shims", default-featur
|
|||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
thiserror = { version = "1", default-features = false, optional = true }
|
||||||
|
|
||||||
|
subtle = { version = "2", default-features = false }
|
||||||
zeroize = { version = "^1.5", default-features = false }
|
zeroize = { version = "^1.5", default-features = false }
|
||||||
rand_core = { version = "0.6", default-features = false }
|
rand_core = { version = "0.6", default-features = false }
|
||||||
|
|
||||||
bitcoin = { version = "0.32", default-features = false }
|
bitcoin = { version = "0.32", default-features = false }
|
||||||
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.9", default-features = false, features = ["secp256k1"], optional = true }
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.10", default-features = false, features = ["secp256k1"], optional = true }
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, optional = true }
|
hex = { version = "0.4", default-features = false, optional = true }
|
||||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
||||||
@@ -46,6 +47,7 @@ std = [
|
|||||||
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
|
||||||
|
"subtle/std",
|
||||||
"zeroize/std",
|
"zeroize/std",
|
||||||
"rand_core/std",
|
"rand_core/std",
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use subtle::{Choice, ConstantTimeEq, ConditionallySelectable};
|
||||||
|
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
||||||
ProjectivePoint,
|
ProjectivePoint,
|
||||||
@@ -5,29 +7,24 @@ use k256::{
|
|||||||
|
|
||||||
use bitcoin::key::XOnlyPublicKey;
|
use bitcoin::key::XOnlyPublicKey;
|
||||||
|
|
||||||
/// Get the x coordinate of a non-infinity, even point. Panics on invalid input.
|
/// Get the x coordinate of a non-infinity point.
|
||||||
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
|
///
|
||||||
|
/// Panics on invalid input.
|
||||||
|
fn x(key: &ProjectivePoint) -> [u8; 32] {
|
||||||
let encoded = key.to_encoded_point(true);
|
let encoded = key.to_encoded_point(true);
|
||||||
assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key");
|
|
||||||
(*encoded.x().expect("point at infinity")).into()
|
(*encoded.x().expect("point at infinity")).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input.
|
/// Convert a non-infinity point to a XOnlyPublicKey (dropping its sign).
|
||||||
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
///
|
||||||
|
/// Panics on invalid input.
|
||||||
|
pub(crate) fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
||||||
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Make a point even by adding the generator until it is even.
|
/// Return if a point must be negated to have an even Y coordinate and be eligible for use.
|
||||||
///
|
pub(crate) fn needs_negation(key: &ProjectivePoint) -> Choice {
|
||||||
/// Returns the even point and the amount of additions required.
|
u8::from(key.to_encoded_point(true).tag()).ct_eq(&u8::from(Tag::CompressedOddY))
|
||||||
#[cfg(any(feature = "std", feature = "hazmat"))]
|
|
||||||
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
|
||||||
let mut c = 0;
|
|
||||||
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
|
||||||
key += ProjectivePoint::GENERATOR;
|
|
||||||
c += 1;
|
|
||||||
}
|
|
||||||
(key, c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
@@ -52,33 +49,38 @@ mod frost_crypto {
|
|||||||
|
|
||||||
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
||||||
///
|
///
|
||||||
/// If passed an odd nonce, it will have the generator added until it is even.
|
/// If passed an odd nonce, the challenge will be negated.
|
||||||
///
|
///
|
||||||
/// If the key is odd, this will panic.
|
/// If either `R` or `A` is the point at infinity, this will panic.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct Hram;
|
pub struct Hram;
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
impl HramTrait<Secp256k1> for Hram {
|
impl HramTrait<Secp256k1> for Hram {
|
||||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
||||||
// Convert the nonce to be even
|
|
||||||
let (R, _) = make_even(*R);
|
|
||||||
|
|
||||||
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
|
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
|
||||||
|
|
||||||
let mut data = Sha256::engine();
|
let mut data = Sha256::engine();
|
||||||
data.input(TAG_HASH.as_ref());
|
data.input(TAG_HASH.as_ref());
|
||||||
data.input(TAG_HASH.as_ref());
|
data.input(TAG_HASH.as_ref());
|
||||||
data.input(&x(&R));
|
data.input(&x(R));
|
||||||
data.input(&x(A));
|
data.input(&x(A));
|
||||||
data.input(m);
|
data.input(m);
|
||||||
|
|
||||||
Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()))
|
let c = Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()));
|
||||||
|
// If the nonce was odd, sign `r - cx` instead of `r + cx`, allowing us to negate `s` at the
|
||||||
|
// end to sign as `-r + cx`
|
||||||
|
<_>::conditional_select(&c, &-c, needs_negation(R))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// BIP-340 Schnorr signature algorithm.
|
/// BIP-340 Schnorr signature algorithm.
|
||||||
///
|
///
|
||||||
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
/// This may panic if called with nonces/a group key which are the point at infinity (which have
|
||||||
|
/// a negligible probability for a well-reasoned caller, even with malicious participants
|
||||||
|
/// present).
|
||||||
|
///
|
||||||
|
/// `verify`, `verify_share` MUST be called after `sign_share` is called. Otherwise, this library
|
||||||
|
/// MAY panic.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Schnorr(FrostSchnorr<Secp256k1, Hram>);
|
pub struct Schnorr(FrostSchnorr<Secp256k1, Hram>);
|
||||||
impl Schnorr {
|
impl Schnorr {
|
||||||
@@ -141,11 +143,7 @@ mod frost_crypto {
|
|||||||
sum: Scalar,
|
sum: Scalar,
|
||||||
) -> Option<Self::Signature> {
|
) -> Option<Self::Signature> {
|
||||||
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
||||||
// Make the R of the final signature even
|
sig.s = <_>::conditional_select(&sum, &-sum, needs_negation(&sig.R));
|
||||||
let offset;
|
|
||||||
(sig.R, offset) = make_even(sig.R);
|
|
||||||
// s = r + cx. Since we added to the r, add to s
|
|
||||||
sig.s += Scalar::from(offset);
|
|
||||||
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
|
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
|
||||||
sig.serialize()[1 ..].try_into().unwrap()
|
sig.serialize()[1 ..].try_into().unwrap()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use rand_core::OsRng;
|
|||||||
|
|
||||||
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
||||||
|
|
||||||
use k256::Scalar;
|
|
||||||
use frost::{
|
use frost::{
|
||||||
curve::Secp256k1,
|
curve::Secp256k1,
|
||||||
Participant,
|
Participant,
|
||||||
@@ -11,7 +10,8 @@ use frost::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bitcoin::hashes::{Hash as HashTrait, sha256::Hash},
|
bitcoin::hashes::{Hash as HashTrait, sha256::Hash},
|
||||||
crypto::{x_only, make_even, Schnorr},
|
crypto::{x_only, Schnorr},
|
||||||
|
wallet::tweak_keys,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -20,8 +20,7 @@ fn test_algorithm() {
|
|||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
const MESSAGE: &[u8] = b"Hello, World!";
|
||||||
|
|
||||||
for keys in keys.values_mut() {
|
for keys in keys.values_mut() {
|
||||||
let (_, offset) = make_even(keys.group_key());
|
*keys = tweak_keys(keys.clone());
|
||||||
*keys = keys.offset(Scalar::from(offset));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let algo = Schnorr::new();
|
let algo = Schnorr::new();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ use bitcoin::{hashes::Hash, consensus::encode::Decodable, TapTweakHash};
|
|||||||
|
|
||||||
use crate::crypto::x_only;
|
use crate::crypto::x_only;
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
use crate::crypto::make_even;
|
use crate::crypto::needs_negation;
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
mod send;
|
mod send;
|
||||||
@@ -39,11 +39,11 @@ pub use send::*;
|
|||||||
/// from being spent via a script. To have keys which have spendable script paths, further offsets
|
/// from being spent via a script. To have keys which have spendable script paths, further offsets
|
||||||
/// from this position must be used.
|
/// from this position must be used.
|
||||||
///
|
///
|
||||||
/// After adding an unspendable script path, the key is incremented until its even. This means the
|
/// After adding an unspendable script path, the key is negated if odd.
|
||||||
/// existence of the unspendable script path may not provable, without an understanding of the
|
///
|
||||||
/// algorithm used here.
|
/// This has a neligible probability of returning keys whose group key is the point at infinity.
|
||||||
#[cfg(feature = "std")]
|
#[cfg(feature = "std")]
|
||||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
pub fn tweak_keys(keys: ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
||||||
// Adds the unspendable script path per
|
// Adds the unspendable script path per
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23
|
// https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23
|
||||||
let keys = {
|
let keys = {
|
||||||
@@ -64,11 +64,14 @@ pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
|||||||
)))
|
)))
|
||||||
};
|
};
|
||||||
|
|
||||||
// This doesn't risk re-introducing a script path as you'd have to find a preimage for the tweak
|
let needs_negation = needs_negation(&keys.group_key());
|
||||||
// hash with whatever increment, or manipulate the key so that the tweak hash and increment
|
keys
|
||||||
// equals the desired offset, yet manipulating the key would change the tweak hash
|
.scale(<_ as subtle::ConditionallySelectable>::conditional_select(
|
||||||
let (_, offset) = make_even(keys.group_key());
|
&Scalar::ONE,
|
||||||
keys.offset(Scalar::from(offset))
|
&-Scalar::ONE,
|
||||||
|
needs_negation,
|
||||||
|
))
|
||||||
|
.expect("scaling keys by 1 or -1 yet interpreted as 0?")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the Taproot address payload for a public key.
|
/// Return the Taproot address payload for a public key.
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ impl SignableTransaction {
|
|||||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
/// A FROST signing machine to produce a Bitcoin transaction.
|
||||||
///
|
///
|
||||||
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
||||||
/// This will panic if either `cache` is called or the message isn't empty.
|
/// This will panic if either `cache`, `from_cache` is called or the message isn't empty.
|
||||||
pub struct TransactionMachine {
|
pub struct TransactionMachine {
|
||||||
tx: SignableTransaction,
|
tx: SignableTransaction,
|
||||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr>>,
|
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr>>,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
|||||||
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
||||||
let mut keys = key_gen(&mut OsRng);
|
let mut keys = key_gen(&mut OsRng);
|
||||||
for keys in keys.values_mut() {
|
for keys in keys.values_mut() {
|
||||||
*keys = tweak_keys(keys);
|
*keys = tweak_keys(keys.clone());
|
||||||
}
|
}
|
||||||
let key = keys.values().next().unwrap().group_key();
|
let key = keys.values().next().unwrap().group_key();
|
||||||
(keys, key)
|
(keys, key)
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey)
|
|||||||
group_key += ProjectivePoint::GENERATOR;
|
group_key += ProjectivePoint::GENERATOR;
|
||||||
}
|
}
|
||||||
for keys in keys.values_mut() {
|
for keys in keys.values_mut() {
|
||||||
*keys = keys.offset(offset);
|
*keys = keys.clone().offset(offset);
|
||||||
}
|
}
|
||||||
let public_key = PublicKey::new(group_key).unwrap();
|
let public_key = PublicKey::new(group_key).unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ borsh = { version = "1", default-features = false, features = ["std", "derive",
|
|||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
serde_json = { version = "1", default-features = false, features = ["std"] }
|
||||||
|
|
||||||
# Cryptography
|
# Cryptography
|
||||||
|
blake2 = { version = "0.10", default-features = false, features = ["std"] }
|
||||||
ciphersuite = { path = "../crypto/ciphersuite", default-features = false, features = ["std", "ristretto"] }
|
ciphersuite = { path = "../crypto/ciphersuite", default-features = false, features = ["std", "ristretto"] }
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../crypto/transcript", default-features = false, features = ["std"] }
|
transcript = { package = "flexible-transcript", path = "../crypto/transcript", default-features = false, features = ["std"] }
|
||||||
|
dkg-pedpop = { path = "../crypto/dkg/pedpop", default-features = false }
|
||||||
frost = { package = "modular-frost", path = "../crypto/frost", default-features = false, features = ["ristretto"] }
|
frost = { package = "modular-frost", path = "../crypto/frost", default-features = false, features = ["ristretto"] }
|
||||||
frost-schnorrkel = { path = "../crypto/schnorrkel", default-features = false }
|
frost-schnorrkel = { path = "../crypto/schnorrkel", default-features = false }
|
||||||
|
|
||||||
@@ -52,8 +54,8 @@ ethereum-serai = { path = "../networks/ethereum", default-features = false, opti
|
|||||||
|
|
||||||
# Monero
|
# Monero
|
||||||
dalek-ff-group = { path = "../crypto/dalek-ff-group", default-features = false, features = ["std"], optional = true }
|
dalek-ff-group = { path = "../crypto/dalek-ff-group", default-features = false, features = ["std"], optional = true }
|
||||||
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4", default-features = false, optional = true }
|
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b", default-features = false, optional = true }
|
||||||
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4", default-features = false, features = ["std", "multisig", "compile-time-generators"], optional = true }
|
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b", default-features = false, features = ["std", "multisig", "compile-time-generators"], optional = true }
|
||||||
|
|
||||||
# Application
|
# Application
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
log = { version = "0.4", default-features = false, features = ["std"] }
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ use rand_chacha::ChaCha20Rng;
|
|||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
use ciphersuite::group::GroupEncoding;
|
use ciphersuite::group::GroupEncoding;
|
||||||
|
use dkg_pedpop::*;
|
||||||
use frost::{
|
use frost::{
|
||||||
curve::{Ciphersuite, Ristretto},
|
curve::{Ciphersuite, Ristretto},
|
||||||
dkg::{
|
dkg::{Participant, ThresholdParams, ThresholdKeys},
|
||||||
DkgError, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, encryption::*, pedpop::*,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
@@ -55,8 +54,8 @@ impl GeneratedKeysDb {
|
|||||||
let mut substrate_keys = vec![];
|
let mut substrate_keys = vec![];
|
||||||
let mut network_keys = vec![];
|
let mut network_keys = vec![];
|
||||||
while !keys_ref.is_empty() {
|
while !keys_ref.is_empty() {
|
||||||
substrate_keys.push(ThresholdKeys::new(ThresholdCore::read(&mut keys_ref).unwrap()));
|
substrate_keys.push(ThresholdKeys::read(&mut keys_ref).unwrap());
|
||||||
let mut these_network_keys = ThresholdKeys::new(ThresholdCore::read(&mut keys_ref).unwrap());
|
let mut these_network_keys = ThresholdKeys::read(&mut keys_ref).unwrap();
|
||||||
N::tweak_keys(&mut these_network_keys);
|
N::tweak_keys(&mut these_network_keys);
|
||||||
network_keys.push(these_network_keys);
|
network_keys.push(these_network_keys);
|
||||||
}
|
}
|
||||||
@@ -66,7 +65,7 @@ impl GeneratedKeysDb {
|
|||||||
fn save_keys<N: Network>(
|
fn save_keys<N: Network>(
|
||||||
txn: &mut impl DbTxn,
|
txn: &mut impl DbTxn,
|
||||||
id: &KeyGenId,
|
id: &KeyGenId,
|
||||||
substrate_keys: &[ThresholdCore<Ristretto>],
|
substrate_keys: &[ThresholdKeys<Ristretto>],
|
||||||
network_keys: &[ThresholdKeys<N::Curve>],
|
network_keys: &[ThresholdKeys<N::Curve>],
|
||||||
) {
|
) {
|
||||||
let mut keys = Zeroizing::new(vec![]);
|
let mut keys = Zeroizing::new(vec![]);
|
||||||
@@ -182,8 +181,9 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
) -> ProcessorMessage {
|
) -> ProcessorMessage {
|
||||||
const SUBSTRATE_KEY_CONTEXT: &str = "substrate";
|
const SUBSTRATE_KEY_CONTEXT: &str = "substrate";
|
||||||
const NETWORK_KEY_CONTEXT: &str = "network";
|
const NETWORK_KEY_CONTEXT: &str = "network";
|
||||||
let context = |id: &KeyGenId, key| {
|
let context = |id: &KeyGenId, key| -> [u8; 32] {
|
||||||
// TODO2: Also embed the chain ID/genesis block
|
// TODO2: Also embed the chain ID/genesis block
|
||||||
|
<blake2::Blake2s256 as blake2::digest::Digest>::digest(
|
||||||
format!(
|
format!(
|
||||||
"Serai Key Gen. Session: {:?}, Network: {:?}, Attempt: {}, Key: {}",
|
"Serai Key Gen. Session: {:?}, Network: {:?}, Attempt: {}, Key: {}",
|
||||||
id.session,
|
id.session,
|
||||||
@@ -191,6 +191,9 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
id.attempt,
|
id.attempt,
|
||||||
key,
|
key,
|
||||||
)
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let rng = |label, id: KeyGenId| {
|
let rng = |label, id: KeyGenId| {
|
||||||
@@ -247,19 +250,10 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
match machine.generate_secret_shares(rng, commitments) {
|
match machine.generate_secret_shares(rng, commitments) {
|
||||||
Ok(res) => Ok(res),
|
Ok(res) => Ok(res),
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
DkgError::ZeroParameter(_, _) |
|
PedPoPError::InvalidCommitments(i) => {
|
||||||
DkgError::InvalidThreshold(_, _) |
|
|
||||||
DkgError::InvalidParticipant(_, _) |
|
|
||||||
DkgError::InvalidSigningSet |
|
|
||||||
DkgError::InvalidShare { .. } => unreachable!("{e:?}"),
|
|
||||||
DkgError::InvalidParticipantQuantity(_, _) |
|
|
||||||
DkgError::DuplicatedParticipant(_) |
|
|
||||||
DkgError::MissingParticipant(_) => {
|
|
||||||
panic!("coordinator sent invalid DKG commitments: {e:?}")
|
|
||||||
}
|
|
||||||
DkgError::InvalidCommitments(i) => {
|
|
||||||
Err(ProcessorMessage::InvalidCommitments { id, faulty: i })?
|
Err(ProcessorMessage::InvalidCommitments { id, faulty: i })?
|
||||||
}
|
}
|
||||||
|
_ => panic!("unknown error: {e:?}"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +391,7 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
m: usize,
|
m: usize,
|
||||||
machine: KeyMachine<C>,
|
machine: KeyMachine<C>,
|
||||||
shares_ref: &mut HashMap<Participant, &[u8]>,
|
shares_ref: &mut HashMap<Participant, &[u8]>,
|
||||||
) -> Result<ThresholdCore<C>, ProcessorMessage> {
|
) -> Result<ThresholdKeys<C>, ProcessorMessage> {
|
||||||
let params = ThresholdParams::new(
|
let params = ThresholdParams::new(
|
||||||
params.t(),
|
params.t(),
|
||||||
params.n(),
|
params.n(),
|
||||||
@@ -422,17 +416,7 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
(match machine.calculate_share(rng, shares) {
|
(match machine.calculate_share(rng, shares) {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(e) => match e {
|
Err(e) => match e {
|
||||||
DkgError::ZeroParameter(_, _) |
|
PedPoPError::InvalidShare { participant, blame } => {
|
||||||
DkgError::InvalidThreshold(_, _) |
|
|
||||||
DkgError::InvalidParticipant(_, _) |
|
|
||||||
DkgError::InvalidSigningSet |
|
|
||||||
DkgError::InvalidCommitments(_) => unreachable!("{e:?}"),
|
|
||||||
DkgError::InvalidParticipantQuantity(_, _) |
|
|
||||||
DkgError::DuplicatedParticipant(_) |
|
|
||||||
DkgError::MissingParticipant(_) => {
|
|
||||||
panic!("coordinator sent invalid DKG shares: {e:?}")
|
|
||||||
}
|
|
||||||
DkgError::InvalidShare { participant, blame } => {
|
|
||||||
Err(ProcessorMessage::InvalidShare {
|
Err(ProcessorMessage::InvalidShare {
|
||||||
id,
|
id,
|
||||||
accuser: params.i(),
|
accuser: params.i(),
|
||||||
@@ -440,6 +424,7 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
blame: Some(blame.map(|blame| blame.serialize())).flatten(),
|
blame: Some(blame.map(|blame| blame.serialize())).flatten(),
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
|
_ => panic!("unknown error: {e:?}"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.complete(),
|
.complete(),
|
||||||
@@ -469,7 +454,7 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
Ok(keys) => keys,
|
Ok(keys) => keys,
|
||||||
Err(msg) => return msg,
|
Err(msg) => return msg,
|
||||||
};
|
};
|
||||||
let these_network_keys =
|
let mut these_network_keys =
|
||||||
match handle_machine(&mut rng, id, params, m, machines.1, &mut shares_ref) {
|
match handle_machine(&mut rng, id, params, m, machines.1, &mut shares_ref) {
|
||||||
Ok(keys) => keys,
|
Ok(keys) => keys,
|
||||||
Err(msg) => return msg,
|
Err(msg) => return msg,
|
||||||
@@ -488,7 +473,6 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut these_network_keys = ThresholdKeys::new(these_network_keys);
|
|
||||||
N::tweak_keys(&mut these_network_keys);
|
N::tweak_keys(&mut these_network_keys);
|
||||||
|
|
||||||
substrate_keys.push(these_substrate_keys);
|
substrate_keys.push(these_substrate_keys);
|
||||||
@@ -557,7 +541,6 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
blame.clone().and_then(|blame| EncryptionKeyProof::read(&mut blame.as_slice()).ok());
|
blame.clone().and_then(|blame| EncryptionKeyProof::read(&mut blame.as_slice()).ok());
|
||||||
|
|
||||||
let substrate_blame = AdditionalBlameMachine::new(
|
let substrate_blame = AdditionalBlameMachine::new(
|
||||||
&mut rand_core::OsRng,
|
|
||||||
context(&id, SUBSTRATE_KEY_CONTEXT),
|
context(&id, SUBSTRATE_KEY_CONTEXT),
|
||||||
params.n(),
|
params.n(),
|
||||||
substrate_commitment_msgs,
|
substrate_commitment_msgs,
|
||||||
@@ -565,7 +548,6 @@ impl<N: Network, D: Db> KeyGen<N, D> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.blame(accuser, accused, substrate_share, substrate_blame);
|
.blame(accuser, accused, substrate_share, substrate_blame);
|
||||||
let network_blame = AdditionalBlameMachine::new(
|
let network_blame = AdditionalBlameMachine::new(
|
||||||
&mut rand_core::OsRng,
|
|
||||||
context(&id, NETWORK_KEY_CONTEXT),
|
context(&id, NETWORK_KEY_CONTEXT),
|
||||||
params.n(),
|
params.n(),
|
||||||
network_commitment_msgs,
|
network_commitment_msgs,
|
||||||
|
|||||||
@@ -648,7 +648,7 @@ impl Network for Bitcoin {
|
|||||||
const MAX_OUTPUTS: usize = MAX_OUTPUTS;
|
const MAX_OUTPUTS: usize = MAX_OUTPUTS;
|
||||||
|
|
||||||
fn tweak_keys(keys: &mut ThresholdKeys<Self::Curve>) {
|
fn tweak_keys(keys: &mut ThresholdKeys<Self::Curve>) {
|
||||||
*keys = tweak_keys(keys);
|
*keys = tweak_keys(keys.clone());
|
||||||
// Also create a scanner to assert these keys, and all expected paths, are usable
|
// Also create a scanner to assert these keys, and all expected paths, are usable
|
||||||
scanner(keys.group_key());
|
scanner(keys.group_key());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ impl<D: Db> Network for Ethereum<D> {
|
|||||||
|
|
||||||
fn tweak_keys(keys: &mut ThresholdKeys<Self::Curve>) {
|
fn tweak_keys(keys: &mut ThresholdKeys<Self::Curve>) {
|
||||||
while PublicKey::new(keys.group_key()).is_none() {
|
while PublicKey::new(keys.group_key()).is_none() {
|
||||||
*keys = keys.offset(<Secp256k1 as Ciphersuite>::F::ONE);
|
*keys = keys.clone().offset(<Secp256k1 as Ciphersuite>::F::ONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ impl Network for Monero {
|
|||||||
keys: ThresholdKeys<Self::Curve>,
|
keys: ThresholdKeys<Self::Curve>,
|
||||||
transaction: SignableTransaction,
|
transaction: SignableTransaction,
|
||||||
) -> Result<Self::TransactionMachine, NetworkError> {
|
) -> Result<Self::TransactionMachine, NetworkError> {
|
||||||
match transaction.0.clone().multisig(&keys) {
|
match transaction.0.clone().multisig(keys) {
|
||||||
Ok(machine) => Ok(machine),
|
Ok(machine) => Ok(machine),
|
||||||
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
|
Err(e) => panic!("failed to create a multisig machine for TX: {e}"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use ciphersuite::group::GroupEncoding;
|
|||||||
use frost::{
|
use frost::{
|
||||||
curve::Ristretto,
|
curve::Ristretto,
|
||||||
Participant,
|
Participant,
|
||||||
dkg::tests::{key_gen, clone_without},
|
tests::{key_gen, clone_without},
|
||||||
};
|
};
|
||||||
|
|
||||||
use sp_application_crypto::{RuntimePublic, sr25519::Public};
|
use sp_application_crypto::{RuntimePublic, sr25519::Public};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use ciphersuite::group::GroupEncoding;
|
|||||||
use frost::{
|
use frost::{
|
||||||
curve::Ristretto,
|
curve::Ristretto,
|
||||||
Participant,
|
Participant,
|
||||||
dkg::tests::{key_gen, clone_without},
|
tests::{key_gen, clone_without},
|
||||||
};
|
};
|
||||||
|
|
||||||
use sp_application_crypto::{RuntimePublic, sr25519::Public};
|
use sp_application_crypto::{RuntimePublic, sr25519::Public};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use rand_core::{RngCore, OsRng};
|
|||||||
use ciphersuite::group::GroupEncoding;
|
use ciphersuite::group::GroupEncoding;
|
||||||
use frost::{
|
use frost::{
|
||||||
Participant, ThresholdKeys,
|
Participant, ThresholdKeys,
|
||||||
dkg::tests::{key_gen, clone_without},
|
tests::{key_gen, clone_without},
|
||||||
};
|
};
|
||||||
|
|
||||||
use serai_db::{DbTxn, Db, MemDb};
|
use serai_db::{DbTxn, Db, MemDb};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::collections::HashMap;
|
|||||||
use rand_core::OsRng;
|
use rand_core::OsRng;
|
||||||
|
|
||||||
use ciphersuite::group::GroupEncoding;
|
use ciphersuite::group::GroupEncoding;
|
||||||
use frost::{Participant, dkg::tests::key_gen};
|
use frost::{Participant, tests::key_gen};
|
||||||
|
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ simple-request = { path = "../../common/request", version = "0.1", optional = tr
|
|||||||
bitcoin = { version = "0.32", optional = true }
|
bitcoin = { version = "0.32", optional = true }
|
||||||
|
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.4", optional = true }
|
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.4", optional = true }
|
||||||
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4", version = "0.1.0", default-features = false, features = ["std"], optional = true }
|
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b", version = "0.1.0", default-features = false, features = ["std"], optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_core = "0.6"
|
rand_core = "0.6"
|
||||||
@@ -48,6 +48,7 @@ hex = "0.4"
|
|||||||
blake2 = "0.10"
|
blake2 = "0.10"
|
||||||
|
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
||||||
|
dkg-musig = { path = "../../crypto/dkg/musig" }
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
||||||
schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" }
|
schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use rand_core::{RngCore, OsRng};
|
|||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
use ciphersuite::{Ciphersuite, Ristretto};
|
use ciphersuite::{Ciphersuite, Ristretto};
|
||||||
use frost::dkg::musig::musig;
|
use dkg_musig::musig;
|
||||||
use schnorrkel::Schnorrkel;
|
use schnorrkel::Schnorrkel;
|
||||||
|
|
||||||
use sp_core::{sr25519::Signature, Pair as PairTrait};
|
use sp_core::{sr25519::Signature, Pair as PairTrait};
|
||||||
@@ -99,7 +99,7 @@ async fn set_values(serai: &Serai, values: &Values) {
|
|||||||
|
|
||||||
assert_eq!(Ristretto::generator() * secret_key, public_key);
|
assert_eq!(Ristretto::generator() * secret_key, public_key);
|
||||||
let threshold_keys =
|
let threshold_keys =
|
||||||
musig::<Ristretto>(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap();
|
musig::<Ristretto>(musig_context(set), Zeroizing::new(secret_key), &[public_key]).unwrap();
|
||||||
|
|
||||||
let sig = frost::tests::sign_without_caching(
|
let sig = frost::tests::sign_without_caching(
|
||||||
&mut OsRng,
|
&mut OsRng,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use sp_core::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use ciphersuite::{Ciphersuite, Ristretto};
|
use ciphersuite::{Ciphersuite, Ristretto};
|
||||||
use frost::dkg::musig::musig;
|
use dkg_musig::musig;
|
||||||
use schnorrkel::Schnorrkel;
|
use schnorrkel::Schnorrkel;
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
@@ -46,8 +46,7 @@ pub async fn set_keys(
|
|||||||
assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]);
|
assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]);
|
||||||
|
|
||||||
threshold_keys.push(
|
threshold_keys.push(
|
||||||
musig::<Ristretto>(&musig_context(set.into()), &Zeroizing::new(secret_key), &pub_keys)
|
musig::<Ristretto>(musig_context(set.into()), Zeroizing::new(secret_key), &pub_keys).unwrap(),
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ workspace = true
|
|||||||
zeroize = { version = "^1.5", features = ["derive"], optional = true }
|
zeroize = { version = "^1.5", features = ["derive"], optional = true }
|
||||||
|
|
||||||
ciphersuite = { path = "../../../crypto/ciphersuite", version = "0.4", default-features = false, features = ["alloc", "ristretto"] }
|
ciphersuite = { path = "../../../crypto/ciphersuite", version = "0.4", default-features = false, features = ["alloc", "ristretto"] }
|
||||||
dkg = { path = "../../../crypto/dkg", version = "0.5", default-features = false }
|
dkg-musig = { path = "../../../crypto/dkg/musig", default-features = false }
|
||||||
|
|
||||||
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
|
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true }
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true }
|
serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true }
|
||||||
@@ -33,7 +33,7 @@ sp-std = { git = "https://github.com/serai-dex/substrate", default-features = fa
|
|||||||
serai-primitives = { path = "../../primitives", default-features = false }
|
serai-primitives = { path = "../../primitives", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = ["zeroize", "ciphersuite/std", "dkg/std", "borsh?/std", "serde?/std", "scale/std", "scale-info/std", "sp-core/std", "sp-std/std", "serai-primitives/std"]
|
std = ["zeroize", "ciphersuite/std", "dkg-musig/std", "borsh?/std", "serde?/std", "scale/std", "scale-info/std", "sp-core/std", "sp-std/std", "serai-primitives/std"]
|
||||||
borsh = ["dep:borsh", "serai-primitives/borsh"]
|
borsh = ["dep:borsh", "serai-primitives/borsh"]
|
||||||
serde = ["dep:serde", "serai-primitives/serde"]
|
serde = ["dep:serde", "serai-primitives/serde"]
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
|
|||||||
@@ -107,8 +107,13 @@ impl Zeroize for KeyPair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The MuSig context for a validator set.
|
/// The MuSig context for a validator set.
|
||||||
pub fn musig_context(set: ValidatorSet) -> Vec<u8> {
|
pub fn musig_context(set: ValidatorSet) -> [u8; 32] {
|
||||||
[b"ValidatorSets-musig_key".as_ref(), &set.encode()].concat()
|
let mut context = [0; 32];
|
||||||
|
const DST: &[u8] = b"ValidatorSets-musig_key";
|
||||||
|
context[.. DST.len()].copy_from_slice(DST);
|
||||||
|
let set = set.encode();
|
||||||
|
context[DST.len() .. (DST.len() + set.len())].copy_from_slice(&set);
|
||||||
|
context
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The MuSig public key for a validator set.
|
/// The MuSig public key for a validator set.
|
||||||
@@ -122,7 +127,7 @@ pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public {
|
|||||||
.expect("invalid participant"),
|
.expect("invalid participant"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Public(dkg::musig::musig_key::<Ristretto>(&musig_context(set), &keys).unwrap().to_bytes())
|
Public(dkg_musig::musig_key_vartime::<Ristretto>(musig_context(set), &keys).unwrap().to_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The message for the set_keys signature.
|
/// The message for the set_keys signature.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ rand_core = { version = "0.6", default-features = false }
|
|||||||
blake2 = "0.10"
|
blake2 = "0.10"
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["ristretto", "secp256k1"] }
|
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["ristretto", "secp256k1"] }
|
||||||
schnorrkel = "0.11"
|
schnorrkel = "0.11"
|
||||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["tests"] }
|
dkg = { path = "../../crypto/dkg", default-features = false }
|
||||||
|
|
||||||
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
|
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ rand_core = { version = "0.6", default-features = false }
|
|||||||
curve25519-dalek = { version = "4", features = ["rand_core"] }
|
curve25519-dalek = { version = "4", features = ["rand_core"] }
|
||||||
|
|
||||||
bitcoin-serai = { path = "../../networks/bitcoin" }
|
bitcoin-serai = { path = "../../networks/bitcoin" }
|
||||||
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4" }
|
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b" }
|
||||||
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4" }
|
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b" }
|
||||||
|
|
||||||
scale = { package = "parity-scale-codec", version = "3" }
|
scale = { package = "parity-scale-codec", version = "3" }
|
||||||
serde = "1"
|
serde = "1"
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ dleq = { path = "../../crypto/dleq", default-features = false }
|
|||||||
schnorr-signatures = { path = "../../crypto/schnorr", default-features = false }
|
schnorr-signatures = { path = "../../crypto/schnorr", default-features = false }
|
||||||
|
|
||||||
dkg = { path = "../../crypto/dkg", default-features = false }
|
dkg = { path = "../../crypto/dkg", default-features = false }
|
||||||
|
dkg-recovery = { path = "../../crypto/dkg/recovery", default-features = false }
|
||||||
|
dkg-dealer = { path = "../../crypto/dkg/dealer", default-features = false }
|
||||||
|
dkg-musig = { path = "../../crypto/dkg/musig", default-features = false }
|
||||||
# modular-frost = { path = "../../crypto/frost", default-features = false }
|
# modular-frost = { path = "../../crypto/frost", default-features = false }
|
||||||
# frost-schnorrkel = { path = "../../crypto/schnorrkel", default-features = false }
|
# frost-schnorrkel = { path = "../../crypto/schnorrkel", default-features = false }
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ pub use dleq;
|
|||||||
pub use schnorr_signatures;
|
pub use schnorr_signatures;
|
||||||
|
|
||||||
pub use dkg;
|
pub use dkg;
|
||||||
|
pub use dkg_recovery;
|
||||||
|
pub use dkg_dealer;
|
||||||
|
pub use dkg_musig;
|
||||||
/*
|
/*
|
||||||
pub use modular_frost;
|
pub use modular_frost;
|
||||||
pub use frost_schnorrkel;
|
pub use frost_schnorrkel;
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ rand_core = { version = "0.6", default-features = false, features = ["getrandom"
|
|||||||
|
|
||||||
curve25519-dalek = "4"
|
curve25519-dalek = "4"
|
||||||
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["secp256k1", "ristretto"] }
|
ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["secp256k1", "ristretto"] }
|
||||||
dkg = { path = "../../crypto/dkg", default-features = false, features = ["tests"] }
|
dkg = { path = "../../crypto/dkg", default-features = false }
|
||||||
|
|
||||||
bitcoin-serai = { path = "../../networks/bitcoin" }
|
bitcoin-serai = { path = "../../networks/bitcoin" }
|
||||||
|
|
||||||
k256 = "0.13"
|
k256 = "0.13"
|
||||||
ethereum-serai = { path = "../../networks/ethereum" }
|
ethereum-serai = { path = "../../networks/ethereum" }
|
||||||
|
|
||||||
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4" }
|
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b" }
|
||||||
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "f19b0f57fe7cbbd643b51091c63de29afb0976e4" }
|
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "a74f41c2270707e340a9cb57fcd97a762d04975b" }
|
||||||
|
|
||||||
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
|
messages = { package = "serai-processor-messages", path = "../../processor/messages" }
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::{
|
|||||||
time::{SystemTime, Duration},
|
time::{SystemTime, Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dkg::{Participant, tests::clone_without};
|
use dkg::Participant;
|
||||||
|
|
||||||
use messages::{coordinator::*, SubstrateContext};
|
use messages::{coordinator::*, SubstrateContext};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{collections::HashMap, time::SystemTime};
|
use std::{collections::HashMap, time::SystemTime};
|
||||||
|
|
||||||
use dkg::{Participant, ThresholdParams, tests::clone_without};
|
use dkg::{Participant, ThresholdParams};
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{BlockHash, PublicKey, EXTERNAL_NETWORKS},
|
primitives::{BlockHash, PublicKey, EXTERNAL_NETWORKS},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use ciphersuite::{Ciphersuite, Ristretto};
|
use ciphersuite::{Ciphersuite, Ristretto};
|
||||||
|
|
||||||
use dockertest::DockerTest;
|
use dockertest::DockerTest;
|
||||||
@@ -15,6 +17,15 @@ mod send;
|
|||||||
pub(crate) const COORDINATORS: usize = 4;
|
pub(crate) const COORDINATORS: usize = 4;
|
||||||
pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1;
|
pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1;
|
||||||
|
|
||||||
|
fn clone_without<K: Clone + core::cmp::Eq + core::hash::Hash, V: Clone>(
|
||||||
|
map: &HashMap<K, V>,
|
||||||
|
without: &K,
|
||||||
|
) -> HashMap<K, V> {
|
||||||
|
let mut res = map.clone();
|
||||||
|
res.remove(without).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
fn new_test(
|
fn new_test(
|
||||||
network: ExternalNetworkId,
|
network: ExternalNetworkId,
|
||||||
) -> (Vec<(Handles, <Ristretto as Ciphersuite>::F)>, DockerTest) {
|
) -> (Vec<(Handles, <Ristretto as Ciphersuite>::F)>, DockerTest) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::{
|
|||||||
time::{SystemTime, Duration},
|
time::{SystemTime, Duration},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dkg::{Participant, tests::clone_without};
|
use dkg::Participant;
|
||||||
|
|
||||||
use messages::{sign::SignId, SubstrateContext};
|
use messages::{sign::SignId, SubstrateContext};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user