From b59f9bb551443a9c02a94dbb861072a54eaedd80 Mon Sep 17 00:00:00 2001 From: Azz Date: Thu, 9 Jan 2025 16:21:59 +0000 Subject: [PATCH 01/30] init mi6 --- Cargo.lock | 1293 +++++++++++++++++++------------- Cargo.toml | 7 + crates/mi6-client/Cargo.toml | 13 + crates/mi6-client/src/lib.rs | 65 ++ crates/mi6-proto/Cargo.toml | 7 + crates/mi6-proto/src/lib.rs | 17 + crates/mi6-proto/src/traits.rs | 5 + crates/mi6-proto/src/wire.rs | 47 ++ crates/mi6-server/Cargo.toml | 13 + crates/mi6-server/src/lib.rs | 52 ++ crates/mi6/Cargo.toml | 11 + crates/mi6/src/main.rs | 4 + 12 files changed, 995 insertions(+), 539 deletions(-) create mode 100644 crates/mi6-client/Cargo.toml create mode 100644 crates/mi6-client/src/lib.rs create mode 100644 crates/mi6-proto/Cargo.toml create mode 100644 crates/mi6-proto/src/lib.rs create mode 100644 crates/mi6-proto/src/traits.rs create mode 100644 crates/mi6-proto/src/wire.rs create mode 100644 crates/mi6-server/Cargo.toml create mode 100644 crates/mi6-server/src/lib.rs create mode 100644 crates/mi6/Cargo.toml create mode 100644 crates/mi6/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index d6440595..cd53d50d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,9 +94,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.1.53" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da226340862e036ab26336dc99ca85311c6b662267c1440e1733890fd688802c" +checksum = "3a754dbb534198644cb8355b8c23f4aaecf03670fb9409242be1fa1e25897ee9" dependencies = [ "alloy-primitives", "num_enum 0.7.3", @@ -119,14 +119,14 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88e1edea70787c33e11197d3f32ae380f3db19e6e061e539a5bcf8184a6b326" +checksum = "69e32ef5c74bbeb1733c37f4ac7f866f8c8af208b7b4265e21af609dcac5bd5e" dependencies = [ - "alloy-eips 0.8.3", + "alloy-eips 0.11.1", "alloy-primitives", "alloy-rlp", - "alloy-serde 0.8.3", + "alloy-serde 0.11.1", "alloy-trie", "auto_impl", "c-kzg", @@ -136,15 +136,15 @@ dependencies = [ [[package]] name = "alloy-consensus-any" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b1bb53f40c0273cd1975573cd457b39213e68584e36d1401d25fd0398a1d65" +checksum = "0fa13b7b1e1e3fedc42f0728103bfa3b4d566d3d42b606db449504d88dbdbdcf" dependencies = [ - "alloy-consensus 0.8.3", - "alloy-eips 0.8.3", + "alloy-consensus 0.11.1", + "alloy-eips 0.11.1", "alloy-primitives", "alloy-rlp", - "alloy-serde 0.8.3", + "alloy-serde 0.11.1", "serde", ] @@ -171,9 +171,9 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0713007d14d88a6edb8e248cddab783b698dbb954a28b8eee4bab21cfb7e578" +checksum = "482f377cebceed4bb1fb5e7970f0805e2ab123d06701be9351b67ed6341e74aa" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "alloy-dyn-abi" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44e3b98c37b3218924cd1d2a8570666b89662be54e5b182643855f783ea68b33" +checksum = "555896f0b8578adb522b1453b6e6cc6704c3027bd0af20058befdde992cee8e9" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -196,7 +196,19 @@ dependencies = [ "itoa", "serde", "serde_json", - "winnow 0.6.22", + "winnow 0.7.2", +] + +[[package]] +name = "alloy-eip2124" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "675264c957689f0fd75f5993a73123c2cc3b5c235a38f5b9037fe6c826bfb2c0" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "thiserror 2.0.11", ] [[package]] @@ -224,9 +236,9 @@ dependencies = [ [[package]] name = "alloy-eip7702" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c986539255fb839d1533c128e190e557e52ff652c9ef62939e233a81dd93f7e" +checksum = "cabf647eb4650c91a9d38cb6f972bb320009e7e9d61765fb688a86f1563b33e8" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -254,15 +266,17 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9fadfe089e9ccc0650473f2d4ef0a28bc015bbca5631d9f0f09e49b557fdb3" +checksum = "5591581ca2ab0b3e7226a4047f9a1bfcf431da1d0cce3752fda609fea3c27e37" dependencies = [ + "alloy-eip2124", "alloy-eip2930", - "alloy-eip7702 0.4.2", + "alloy-eip7702 0.5.0", "alloy-primitives", "alloy-rlp", - "alloy-serde 0.8.3", + "alloy-serde 0.11.1", + "auto_impl", "c-kzg", "derive_more", "once_cell", @@ -283,9 +297,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "731ea743b3d843bc657e120fb1d1e9cc94f5dab8107e35a82125a63e6420a102" +checksum = "4012581681b186ba0882007ed873987cc37f86b1b488fe6b91d5efd0b585dc41" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -309,9 +323,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e29040b9d5fe2fb70415531882685b64f8efd08dfbd6cc907120650504821105" +checksum = "762414662d793d7aaa36ee3af6928b6be23227df1681ce9c039f6f11daadef64" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -344,20 +358,20 @@ dependencies = [ [[package]] name = "alloy-network" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510cc00b318db0dfccfdd2d032411cfae64fc144aef9679409e014145d3dacc4" +checksum = "8be03f2ebc00cf88bd06d3c6caf387dceaa9c7e6b268216779fa68a9bf8ab4e6" dependencies = [ - "alloy-consensus 0.8.3", + "alloy-consensus 0.11.1", "alloy-consensus-any", - "alloy-eips 0.8.3", - "alloy-json-rpc 0.8.3", - "alloy-network-primitives 0.8.3", + "alloy-eips 0.11.1", + "alloy-json-rpc 0.11.1", + "alloy-network-primitives 0.11.1", "alloy-primitives", "alloy-rpc-types-any", - "alloy-rpc-types-eth 0.8.3", - "alloy-serde 0.8.3", - "alloy-signer 0.8.3", + "alloy-rpc-types-eth 0.11.1", + "alloy-serde 0.11.1", + "alloy-signer 0.11.1", "alloy-sol-types", "async-trait", "auto_impl", @@ -381,14 +395,14 @@ dependencies = [ [[package]] name = "alloy-network-primitives" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9081c099e798b8a2bba2145eb82a9a146f01fc7a35e9ab6e7b43305051f97550" +checksum = "3a00ce618ae2f78369918be0c20f620336381502c83b6ed62c2f7b2db27698b0" dependencies = [ - "alloy-consensus 0.8.3", - "alloy-eips 0.8.3", + "alloy-consensus 0.11.1", + "alloy-eips 0.11.1", "alloy-primitives", - "alloy-serde 0.8.3", + "alloy-serde 0.11.1", "serde", ] @@ -411,9 +425,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788bb18e8f61d5d9340b52143f27771daf7e1dccbaf2741621d2493f9debf52e" +checksum = "478bedf4d24e71ea48428d1bc278553bd7c6ae07c30ca063beb0b09fe58a9e74" dependencies = [ "alloy-rlp", "bytes", @@ -422,7 +436,7 @@ dependencies = [ "derive_more", "foldhash", "hashbrown 0.15.2", - "indexmap 2.7.0", + "indexmap 2.7.1", "itoa", "k256", "keccak-asm", @@ -430,7 +444,7 @@ dependencies = [ "proptest", "rand", "ruint", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "sha3", "tiny-keccak", @@ -497,9 +511,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f542548a609dca89fcd72b3b9f355928cf844d4363c5eed9c5273a3dd225e097" +checksum = "3d6c1d995bff8d011f7cd6c81820d51825e6e06d6db73914c1630ecf544d83d6" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -508,13 +522,13 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a833d97bf8a5f0f878daf2c8451fff7de7f9de38baa5a45d936ec718d81255a" +checksum = "a40e1ef334153322fd878d07e86af7a529bcb86b2439525920a88eba87bcf943" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -567,13 +581,13 @@ dependencies = [ [[package]] name = "alloy-rpc-types-any" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed98e1af55a7d856bfa385f30f63d8d56be2513593655c904a8f4a7ec963aa3e" +checksum = "318ae46dd12456df42527c3b94c1ae9001e1ceb707f7afe2c7807ac4e49ebad9" dependencies = [ "alloy-consensus-any", - "alloy-rpc-types-eth 0.8.3", - "alloy-serde 0.8.3", + "alloy-rpc-types-eth 0.11.1", + "alloy-serde 0.11.1", ] [[package]] @@ -612,22 +626,22 @@ dependencies = [ [[package]] name = "alloy-rpc-types-eth" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8737d7a6e37ca7bba9c23e9495c6534caec6760eb24abc9d5ffbaaba147818e1" +checksum = "8b4dbee4d82f8a22dde18c28257bed759afeae7ba73da4a1479a039fd1445d04" dependencies = [ - "alloy-consensus 0.8.3", + "alloy-consensus 0.11.1", "alloy-consensus-any", - "alloy-eips 0.8.3", - "alloy-network-primitives 0.8.3", + "alloy-eips 0.11.1", + "alloy-network-primitives 0.11.1", "alloy-primitives", "alloy-rlp", - "alloy-serde 0.8.3", + "alloy-serde 0.11.1", "alloy-sol-types", - "derive_more", - "itertools 0.13.0", + "itertools 0.14.0", "serde", "serde_json", + "thiserror 2.0.11", ] [[package]] @@ -643,9 +657,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5851bf8d5ad33014bd0c45153c603303e730acc8a209450a7ae6b4a12c2789e2" +checksum = "8732058f5ca28c1d53d241e8504620b997ef670315d7c8afab856b3e3b80d945" dependencies = [ "alloy-primitives", "serde", @@ -668,13 +682,14 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e10ca565da6500cca015ba35ee424d59798f2e1b85bc0dd8f81dafd401f029a" +checksum = "f96b3526fdd779a4bd0f37319cfb4172db52a7ac24cdbb8804b72091c18e1701" dependencies = [ "alloy-primitives", "async-trait", "auto_impl", + "either", "elliptic-curve", "k256", "thiserror 2.0.11", @@ -698,14 +713,14 @@ dependencies = [ [[package]] name = "alloy-signer-local" -version = "0.8.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47fababf5a745133490cde927d48e50267f97d3d1209b9fc9f1d1d666964d172" +checksum = "fe8f78cd6b7501c7e813a1eb4a087b72d23af51f5bb66d4e948dc840bdd207d8" dependencies = [ - "alloy-consensus 0.8.3", - "alloy-network 0.8.3", + "alloy-consensus 0.11.1", + "alloy-network 0.11.1", "alloy-primitives", - "alloy-signer 0.8.3", + "alloy-signer 0.11.1", "async-trait", "k256", "rand", @@ -714,42 +729,42 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a07b74d48661ab2e4b50bb5950d74dbff5e61dd8ed03bb822281b706d54ebacb" +checksum = "a2708e27f58d747423ae21d31b7a6625159bd8d867470ddd0256f396a68efa11" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "alloy-sol-macro-expander" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19cc9c7f20b90f9be1a8f71a3d8e283a43745137b0837b1a1cb13159d37cad72" +checksum = "c6b7984d7e085dec382d2c5ef022b533fcdb1fe6129200af30ebf5afddb6a361" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck 0.5.0", - "indexmap 2.7.0", + "indexmap 2.7.1", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713b7e6dfe1cb2f55c80fb05fd22ed085a1b4e48217611365ed0ae598a74c6ac" +checksum = "33d6a9fc4ed1a3c70bdb2357bec3924551c1a59f24e5a04a74472c755b37f87d" dependencies = [ "alloy-json-abi", "const-hex", @@ -758,25 +773,25 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.95", + "syn 2.0.98", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eda2711ab2e1fb517fc6e2ffa9728c9a232e296d16810810e6957b781a1b8bc" +checksum = "1b1b3e9a48a6dd7bb052a111c8d93b5afc7956ed5e2cb4177793dc63bb1d2a36" dependencies = [ "serde", - "winnow 0.6.22", + "winnow 0.7.2", ] [[package]] name = "alloy-sol-types" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b478bc9c0c4737a04cd976accde4df7eba0bdc0d90ad6ff43d58bc93cf79c1" +checksum = "6044800da35c38118fd4b98e18306bd3b91af5dedeb54c1b768cf1b4fb68f549" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -848,7 +863,7 @@ dependencies = [ "alloy-transport", "futures", "http", - "rustls 0.23.20", + "rustls 0.23.23", "serde_json", "tokio", "tokio-tungstenite", @@ -858,9 +873,9 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6917c79e837aa7b77b7a6dae9f89cbe15313ac161c4d3cfaf8909ef21f3d22d8" +checksum = "d95a94854e420f07e962f7807485856cde359ab99ab6413883e15235ad996e8b" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -937,11 +952,12 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", + "once_cell", "windows-sys 0.59.0", ] @@ -1000,7 +1016,7 @@ source = "git+https://github.com/arkworks-rs/crypto-primitives/#b13983815e5b3a0f dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1078,7 +1094,7 @@ version = "0.4.2" source = "git+https://github.com/chainwayxyz/algebra/?branch=new-ate-loop#ac23fde284ca4d7ede298018f7866ce8ce64467f" dependencies = [ "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1102,7 +1118,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1194,7 +1210,7 @@ source = "git+https://github.com/chainwayxyz/algebra/?branch=new-ate-loop#ac23fd dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1222,7 +1238,8 @@ dependencies = [ [[package]] name = "ark-std" version = "0.4.0" -source = "git+https://github.com/arkworks-rs/std/#db4367e68ff60da31ac759831e38f60171f4e03d" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "colored", "num-traits", @@ -1256,7 +1273,7 @@ dependencies = [ "bitvm", "blake3", "proptest", - "secp256k1", + "secp256k1 0.29.1", "strata-bridge-primitives", "strata-bridge-test-utils", ] @@ -1294,18 +1311,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1350,13 +1367,13 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "auto_impl" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" +checksum = "e12882f59de5360c748c4cbf569a042d5fb0eb515f7bea9c1f470b47f6ffbd73" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1427,7 +1444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "futures-core", - "getrandom", + "getrandom 0.2.15", "instant", "pin-project-lite", "rand", @@ -1490,6 +1507,43 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bdk_chain" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4955734f97b2baed3f36d16ae7c203fdde31ae85391ac44ee3cbcaf0886db5ce" +dependencies = [ + "bdk_core", + "bitcoin", + "miniscript", + "serde", +] + +[[package]] +name = "bdk_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" +dependencies = [ + "bitcoin", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "bdk_wallet" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13c947be940d32a91b876fc5223a6d839a40bc219496c5c78af74714b1b3f7" +dependencies = [ + "bdk_chain", + "bitcoin", + "miniscript", + "rand_core", + "serde", + "serde_json", +] + [[package]] name = "bech32" version = "0.11.0" @@ -1517,10 +1571,10 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -1528,7 +1582,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1561,7 +1615,7 @@ dependencies = [ "bitcoin_hashes", "hex-conservative", "hex_lit", - "secp256k1", + "secp256k1 0.29.1", "serde", ] @@ -1575,7 +1629,7 @@ dependencies = [ "bitcoin", "borsh", "hex-conservative", - "secp256k1", + "secp256k1 0.29.1", "serde", "thiserror 2.0.11", ] @@ -1625,7 +1679,7 @@ dependencies = [ "bitcoin", "clap", "console_error_panic_hook", - "getrandom", + "getrandom 0.2.15", "lazy_static", "serde", "serde-wasm-bindgen", @@ -1699,9 +1753,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -1772,9 +1826,9 @@ dependencies = [ [[package]] name = "blake2b_simd" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" dependencies = [ "arrayref", "arrayvec", @@ -1824,9 +1878,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378725facc195f1a538864863f6de233b500a8862747e7f165078a419d5e874" +checksum = "47c79a94619fade3c0b887670333513a67ac28a6a7e653eb260bf0d4103db38d" dependencies = [ "cc", "glob", @@ -1849,9 +1903,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.3" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" dependencies = [ "borsh-derive", "cfg_aliases", @@ -1859,15 +1913,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.3" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" dependencies = [ "once_cell", "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1886,9 +1940,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byte-slice-cast" @@ -1898,9 +1952,9 @@ checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" [[package]] name = "bytecheck" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c8f430744b23b54ad15161fcbc22d82a29b73eacbe425fea23ec822600bc6f" +checksum = "50690fb3370fb9fe3550372746084c46f2ac8c9685c583d2be10eefd89d3d1a3" dependencies = [ "bytecheck_derive", "ptr_meta", @@ -1910,13 +1964,13 @@ dependencies = [ [[package]] name = "bytecheck_derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523363cbe1df49b68215efdf500b103ac3b0fb4836aed6d15689a076eadb8fff" +checksum = "efb7846e0cb180355c2dec69e721edafa36919850f1a9f52ffba4ebc0393cb71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1936,7 +1990,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -1947,9 +2001,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" dependencies = [ "serde", ] @@ -1966,9 +2020,9 @@ dependencies = [ [[package]] name = "bzip2-sys" -version = "0.1.11+1.0.8" +version = "0.1.12+1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9" dependencies = [ "cc", "libc", @@ -2016,7 +2070,7 @@ checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" dependencies = [ "camino", "cargo-platform", - "semver 1.0.24", + "semver 1.0.25", "serde", "serde_json", "thiserror 1.0.69", @@ -2030,22 +2084,22 @@ checksum = "3fce8dd7fcfcbf3a0a87d8f515194b49d6135acab73e18bd380d1d93bb1a15eb" dependencies = [ "clap", "heck 0.4.1", - "indexmap 2.7.0", + "indexmap 2.7.1", "log", "proc-macro2", "quote", "serde", "serde_json", - "syn 2.0.95", + "syn 2.0.98", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.2.7" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7" +checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9" dependencies = [ "jobserver", "libc", @@ -2133,9 +2187,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.24" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9560b07a799281c7e0958b9296854d6fafd4c5f31444a7e5bb1ad6dde5ccf1bd" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" dependencies = [ "clap_builder", "clap_derive", @@ -2143,9 +2197,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.24" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874e0dd3eb68bf99058751ac9712f622e61e6f393a94f7128fa26e3f02f5c7cd" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" dependencies = [ "anstream", "anstyle", @@ -2155,14 +2209,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.24" +version = "4.5.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2248,6 +2302,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_format" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -2344,9 +2418,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -2458,9 +2532,9 @@ dependencies = [ [[package]] name = "crunchy" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" [[package]] name = "crypto-bigint" @@ -2515,7 +2589,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2526,7 +2600,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2623,9 +2697,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" [[package]] name = "deadpool" @@ -2684,7 +2758,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2704,7 +2778,7 @@ checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", "unicode-xid", ] @@ -2788,7 +2862,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2868,7 +2942,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2898,6 +2972,7 @@ dependencies = [ "ff 0.13.0", "generic-array 0.14.7", "group 0.13.0", + "hkdf", "pem-rfc7468", "pkcs8", "rand_core", @@ -2939,7 +3014,7 @@ checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -2959,14 +3034,14 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -3157,7 +3232,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -3270,7 +3345,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -3355,29 +3430,28 @@ dependencies = [ "cfg-if 1.0.0", "js-sys", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "git2" -version = "0.19.0" +name = "getrandom" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724" +checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" dependencies = [ - "bitflags 2.6.0", + "cfg-if 1.0.0", "libc", - "libgit2-sys", - "log", - "url", + "wasi 0.13.3+wasi-0.2.2", + "windows-targets 0.52.6", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "glob" version = "0.3.2" @@ -3465,7 +3539,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.7.0", + "indexmap 2.7.1", "slab", "tokio", "tokio-util", @@ -3644,9 +3718,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -3656,9 +3730,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -3686,12 +3760,12 @@ dependencies = [ "hyper", "hyper-util", "log", - "rustls 0.23.20", + "rustls 0.23.23", "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", - "webpki-roots 0.26.7", + "webpki-roots 0.26.8", ] [[package]] @@ -3880,7 +3954,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -3927,7 +4001,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -3949,9 +4023,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3960,9 +4034,9 @@ dependencies = [ [[package]] name = "indicatif" -version = "0.17.9" +version = "0.17.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf675b85ed934d3c67b5c5469701eec7db22689d0a2139d856e0925fa28b281" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" dependencies = [ "console", "number_prefix", @@ -4015,9 +4089,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" [[package]] name = "is_terminal_polyfill" @@ -4043,6 +4117,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.14" @@ -4080,9 +4163,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", @@ -4102,9 +4185,9 @@ dependencies = [ [[package]] name = "jsonrpsee" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5c71d8c1a731cc4227c2f698d377e7848ca12c8a48866fc5e6951c43a4db843" +checksum = "834af00800e962dee8f7bfc0f60601de215e73e78e5497d733a2919da837d3c8" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -4120,9 +4203,9 @@ dependencies = [ [[package]] name = "jsonrpsee-client-transport" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "548125b159ba1314104f5bb5f38519e03a41862786aa3925cf349aae9cdd546e" +checksum = "def0fd41e2f53118bd1620478d12305b2c75feef57ea1f93ef70568c98081b7e" dependencies = [ "base64 0.22.1", "futures-channel", @@ -4131,9 +4214,9 @@ dependencies = [ "http", "jsonrpsee-core", "pin-project", - "rustls 0.23.20", + "rustls 0.23.23", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.3.4", "soketto", "thiserror 1.0.69", "tokio", @@ -4145,9 +4228,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2882f6f8acb9fdaec7cefc4fd607119a9bd709831df7d7672a1d3b644628280" +checksum = "76637f6294b04e747d68e69336ef839a3493ca62b35bf488ead525f7da75c5bb" dependencies = [ "async-trait", "bytes", @@ -4160,7 +4243,7 @@ dependencies = [ "parking_lot", "pin-project", "rand", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "serde", "serde_json", "thiserror 1.0.69", @@ -4172,9 +4255,9 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3638bc4617f96675973253b3a45006933bde93c2fd8a6170b33c777cc389e5b" +checksum = "87c24e981ad17798bbca852b0738bfb7b94816ed687bd0d5da60bfa35fa0fdc3" dependencies = [ "async-trait", "base64 0.22.1", @@ -4184,8 +4267,8 @@ dependencies = [ "hyper-util", "jsonrpsee-core", "jsonrpsee-types", - "rustls 0.23.20", - "rustls-platform-verifier", + "rustls 0.23.23", + "rustls-platform-verifier 0.3.4", "serde", "serde_json", "thiserror 1.0.69", @@ -4197,22 +4280,22 @@ dependencies = [ [[package]] name = "jsonrpsee-proc-macros" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06c01ae0007548e73412c08e2285ffe5d723195bf268bce67b1b77c3bb2a14d" +checksum = "6fcae0c6c159e11541080f1f829873d8f374f81eda0abc67695a13fc8dc1a580" dependencies = [ "heck 0.5.0", "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "jsonrpsee-server" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82ad8ddc14be1d4290cd68046e7d1d37acd408efed6d3ca08aefcc3ad6da069c" +checksum = "66b7a3df90a1a60c3ed68e7ca63916b53e9afa928e33531e87f61a9c8e9ae87b" dependencies = [ "futures-util", "http", @@ -4237,9 +4320,9 @@ dependencies = [ [[package]] name = "jsonrpsee-types" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a178c60086f24cc35bb82f57c651d0d25d99c4742b4d335de04e97fa1f08a8a1" +checksum = "ddb81adb1a5ae9182df379e374a79e24e992334e7346af4d065ae5b2acb8d4c6" dependencies = [ "http", "serde", @@ -4249,9 +4332,9 @@ dependencies = [ [[package]] name = "jsonrpsee-wasm-client" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01cd500915d24ab28ca17527e23901ef1be6d659a2322451e1045532516c25" +checksum = "42e41af42ca39657313748174d02766e5287d3a57356f16756dbd8065b933977" dependencies = [ "jsonrpsee-client-transport", "jsonrpsee-core", @@ -4260,9 +4343,9 @@ dependencies = [ [[package]] name = "jsonrpsee-ws-client" -version = "0.24.7" +version = "0.24.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe322e0896d0955a3ebdd5bf813571c53fea29edd713bc315b76620b327e86d" +checksum = "6f4f3642a292f5b76d8a16af5c88c16a0860f2ccc778104e5c848b28183d9538" dependencies = [ "http", "jsonrpsee-client-transport", @@ -4309,6 +4392,16 @@ dependencies = [ "signature", ] +[[package]] +name = "kanal" +version = "0.1.0-pre8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05d55519627edaf7fd0f29981f6dc03fb52df3f5b257130eb8d0bf2801ea1d7" +dependencies = [ + "futures-core", + "lock_api", +] + [[package]] name = "keccak" version = "0.1.5" @@ -4358,7 +4451,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -4376,18 +4469,6 @@ version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" -[[package]] -name = "libgit2-sys" -version = "0.17.0+1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10472326a8a6477c3c20a64547b0059e4b0d086869eee31e6d7da728a8eb7224" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libloading" version = "0.8.6" @@ -4410,7 +4491,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", "redox_syscall", ] @@ -4426,18 +4507,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "libz-sys" -version = "1.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9b68e50e6e0b26f672573834882eb57759f6db9b3be2ea3c35c91188bb4eaa" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -4462,9 +4531,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lru" @@ -4507,7 +4576,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -4550,7 +4619,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -4559,6 +4628,49 @@ dependencies = [ "paste", ] +[[package]] +name = "mi6" +version = "0.1.0" +dependencies = [ + "bdk_wallet", + "mi6-proto", + "musig2 0.2.3", + "tokio", +] + +[[package]] +name = "mi6-client" +version = "0.1.0" +dependencies = [ + "kanal", + "mi6-proto", + "quinn", + "rkyv", + "terrors", + "tokio", + "tracing", +] + +[[package]] +name = "mi6-proto" +version = "0.1.0" +dependencies = [ + "rkyv", +] + +[[package]] +name = "mi6-server" +version = "0.1.0" +dependencies = [ + "kanal", + "mi6-proto", + "quinn", + "rkyv", + "terrors", + "tokio", + "tracing", +] + [[package]] name = "mime" version = "0.3.17" @@ -4579,22 +4691,23 @@ checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ "bech32", "bitcoin", + "serde", ] [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", ] [[package]] name = "minreq" -version = "2.13.0" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a8e50e917e18a37d500d27d40b7bc7d127e71c0c94fb2d83f43b4afd308390" +checksum = "da0c420feb01b9fb5061f8c8f452534361dd783756dcf38ec45191ce55e7a161" dependencies = [ "log", "once_cell", @@ -4632,7 +4745,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.48.0", ] @@ -4643,7 +4756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.52.0", ] @@ -4676,7 +4789,7 @@ checksum = "1bb5c1d8184f13f7d0ccbeeca0def2f9a181bce2624302793005f5ca8aa62e5e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -4689,19 +4802,34 @@ dependencies = [ "hmac", "once_cell", "rand", - "secp", - "secp256k1", + "secp 0.3.0", + "secp256k1 0.29.1", "serde", "serdect", "sha2", "subtle", ] +[[package]] +name = "musig2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd55e6cfd1ddd92698a3e456432e16310f0810faad83ac732e2466ed0961b46d" +dependencies = [ + "base16ct", + "hmac", + "once_cell", + "secp 0.4.1", + "secp256k1 0.30.0", + "sha2", + "subtle", +] + [[package]] name = "native-tls" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" dependencies = [ "libc", "log", @@ -4731,7 +4859,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if 1.0.0", "cfg_aliases", "libc", @@ -4945,16 +5073,7 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.95", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", + "syn 2.0.98", ] [[package]] @@ -4994,9 +5113,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "openssl" @@ -5004,7 +5123,7 @@ version = "0.10.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if 1.0.0", "foreign-types 0.3.2", "libc", @@ -5021,14 +5140,14 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "openssl-probe" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" @@ -5390,28 +5509,30 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.12" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" +checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" dependencies = [ "arrayvec", "bitvec", "byte-slice-cast", + "const_format", "impl-trait-for-tuples", "parity-scale-codec-derive", + "rustversion", "serde", ] [[package]] name = "parity-scale-codec-derive" -version = "3.6.12" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" +checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.98", ] [[package]] @@ -5546,22 +5667,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -5626,12 +5747,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "483f8c21f64f3ea09fe0f30f5d48c3e8eefe5dac9129f0075f76593b4c1da705" +checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -5670,7 +5791,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.22.22", + "toml_edit 0.22.24", ] [[package]] @@ -5716,14 +5837,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -5736,7 +5857,7 @@ checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.6.0", + "bitflags 2.8.0", "lazy_static", "num-traits", "rand", @@ -5750,9 +5871,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes", "prost-derive", @@ -5760,15 +5881,15 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -5789,7 +5910,7 @@ dependencies = [ "strata-proofimpl-btc-blockspace", "strata-state", "tokio", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", ] [[package]] @@ -5809,7 +5930,7 @@ checksum = "ca414edb151b4c8d125c12566ab0d74dc9cdba36fb80eb7b848c15f495fd32d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -5828,8 +5949,8 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.0", - "rustls 0.23.20", + "rustc-hash 2.1.1", + "rustls 0.23.23", "socket2", "thiserror 2.0.11", "tokio", @@ -5843,12 +5964,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" dependencies = [ "bytes", - "getrandom", + "getrandom 0.2.15", "rand", "ring", - "rustc-hash 2.1.0", - "rustls 0.23.20", + "rustc-hash 2.1.1", + "rustls 0.23.23", "rustls-pki-types", + "rustls-platform-verifier 0.4.0", "slab", "thiserror 2.0.11", "tinyvec", @@ -5858,9 +5980,9 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" dependencies = [ "cfg_aliases", "libc", @@ -5922,7 +6044,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", ] [[package]] @@ -5975,7 +6097,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -5984,7 +6106,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", "thiserror 1.0.69", ] @@ -6072,7 +6194,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.20", + "rustls 0.23.23", "rustls-pemfile", "rustls-pki-types", "serde", @@ -6090,7 +6212,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.7", + "webpki-roots 0.26.8", "windows-registry", ] @@ -6121,15 +6243,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -6282,7 +6403,7 @@ dependencies = [ "borsh", "bytemuck", "bytes", - "getrandom", + "getrandom 0.2.15", "hex", "lazy-regex", "prost", @@ -6296,7 +6417,7 @@ dependencies = [ "risc0-zkp", "risc0-zkvm-platform", "rrs-lib", - "semver 1.0.24", + "semver 1.0.25", "serde", "sha2", "stability", @@ -6312,21 +6433,21 @@ checksum = "3707a298cacf4d665258b9e976d422401dcfe833f50794fa1e7c20d15ab45e7f" dependencies = [ "bytemuck", "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.15", "libm", "stability", ] [[package]] name = "rkyv" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11a153aec4a6ab60795f8ebe2923c597b16b05bb1504377451e705ef1a45323" +checksum = "1e147371c75553e1e2fcdb483944a8540b8438c31426279553b9a8182a9b7b65" dependencies = [ "bytecheck", "bytes", "hashbrown 0.15.2", - "indexmap 2.7.0", + "indexmap 2.7.1", "munge", "ptr_meta", "rancor", @@ -6338,13 +6459,13 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb382a4d9f53bd5c0be86b10d8179c3f8a14c30bf774ff77096ed6581e35981" +checksum = "246b40ac189af6c675d124b802e8ef6d5246c53e17367ce9501f8f66a81abb7a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -6457,9 +6578,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc-hex" @@ -6482,16 +6603,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.24", + "semver 1.0.25", ] [[package]] name = "rustix" -version = "0.38.43" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -6512,9 +6633,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.20" +version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5065c3f250cbd332cd894be57c40fa52387247659b14a2d6041d121547903b1b" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "log", "once_cell", @@ -6561,9 +6682,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] @@ -6579,16 +6700,37 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.20", + "rustls 0.23.23", "rustls-native-certs 0.7.3", "rustls-platform-verifier-android", "rustls-webpki 0.102.8", "security-framework 2.11.1", "security-framework-sys", - "webpki-roots 0.26.7", + "webpki-roots 0.26.8", "winapi 0.3.9", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c7dc240fec5517e6c4eab3310438636cfe6391dfc345ba013109909a90d136" +dependencies = [ + "core-foundation 0.9.4", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.23", + "rustls-native-certs 0.7.3", + "rustls-platform-verifier-android", + "rustls-webpki 0.102.8", + "security-framework 2.11.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" @@ -6636,9 +6778,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -6670,14 +6812,14 @@ dependencies = [ "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "scc" -version = "2.3.0" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28e1c91382686d21b5ac7959341fcb9780fa7c03773646995a87c950fa7be640" +checksum = "ea091f6cac2595aa38993f04f4ee692ed43757035c36e67c180b6828356385b1" dependencies = [ "sdd", ] @@ -6722,9 +6864,9 @@ dependencies = [ [[package]] name = "sdd" -version = "3.0.5" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" +checksum = "b07779b9b918cc05650cb30f404d4d7835d26df37c235eded8a6832e2fb82cca" [[package]] name = "sec1" @@ -6749,12 +6891,24 @@ dependencies = [ "base16ct", "once_cell", "rand", - "secp256k1", + "secp256k1 0.29.1", "serde", "serdect", "subtle", ] +[[package]] +name = "secp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ed54b1141d8cec428d8a4abf01282755ba4e4c8a621dd23fa2e0ed761814c2" +dependencies = [ + "base16ct", + "once_cell", + "secp256k1 0.30.0", + "subtle", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -6767,6 +6921,17 @@ dependencies = [ "serde", ] +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand", + "secp256k1-sys", +] + [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -6782,7 +6947,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -6796,7 +6961,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -6824,9 +6989,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" dependencies = [ "serde", ] @@ -6880,14 +7045,14 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" dependencies = [ "itoa", "memchr", @@ -6936,7 +7101,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_derive", "serde_json", @@ -6953,7 +7118,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -6988,7 +7153,7 @@ checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -7111,9 +7276,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" dependencies = [ "serde", ] @@ -7156,9 +7321,9 @@ dependencies = [ [[package]] name = "sp1-build" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2e8eaf2d29f8e7170c1d901e7c0133399c7fe557aedea659f171a86f34a9f9" +checksum = "cb3ab2b8e8bad6d5ab1235ac31f777a0d4487a3d79b275af61008e58cec40391" dependencies = [ "anyhow", "cargo_metadata", @@ -7169,9 +7334,9 @@ dependencies = [ [[package]] name = "sp1-core-executor" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "386bdb534e0cf3643dd9dd67ed06933730913a03dd1ab0e64893cd969c1da57a" +checksum = "cd0552baf7793bb2f43967002ac73e0f7860c77f5e6b70875faef771ebd89cbc" dependencies = [ "bincode", "bytemuck", @@ -7208,9 +7373,9 @@ dependencies = [ [[package]] name = "sp1-core-machine" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89881a2078d8e32933e9687a0f146e470842f3674f8a813524d80b7dc42eb7e9" +checksum = "0ba0d397750fd913aa54604fd363a9a46637effc2363ac090e45a7700c8d8c89" dependencies = [ "bincode", "cbindgen", @@ -7265,9 +7430,9 @@ dependencies = [ [[package]] name = "sp1-cuda" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ef50ff96d561bb7e8a000ec35317863a517f7c68f9d1231e1483c4e93c174a1" +checksum = "404e9ea41a7ed135fc05b58efc37e30255192fd6ba594f9d10b74e9a938b5cf0" dependencies = [ "bincode", "ctrlc", @@ -7282,9 +7447,9 @@ dependencies = [ [[package]] name = "sp1-curves" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b325b91b40643b36738a4abcdfde81ba8b158e04c3e1a2d94a1942df84e03c8" +checksum = "c7441109fa83ba456341b21910aa9b8b08a09c16b9ca38215ebc83d9d790a62e" dependencies = [ "cfg-if 1.0.0", "dashu", @@ -7304,9 +7469,9 @@ dependencies = [ [[package]] name = "sp1-derive" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d6abd68038a6f688601cdd0cbec17b56f56e5d10e3f34f1a42bee8cc7ca2c" +checksum = "0527cf1c71561d7a83059a53733f45504b5e71ff63a68da8cd9705bb53a3d1c6" dependencies = [ "quote", "syn 1.0.109", @@ -7314,29 +7479,30 @@ dependencies = [ [[package]] name = "sp1-helper" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfeb036e069d9c621c99579e90175e71f2a59232b2ecdb3c44a31f5e601e5259" +checksum = "39835e1626747d7081f9656f5b6176a4e3749240cf03f0d8af3e0e6b0658d710" dependencies = [ "sp1-build", ] [[package]] name = "sp1-lib" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2378a017c2159e1ab89ed73ff797771ab8b00b11ee1d86852c00c2c9fabc76ce" +checksum = "fab1b5989a446f294724cebab0e759ffd7034ba93d2aa7b97045303f7c50bf74" dependencies = [ "bincode", + "elliptic-curve", "serde", "sp1-primitives", ] [[package]] name = "sp1-primitives" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c741d8c2907ac96f71445ed8d7abb0fdbea115c6becbcbc7c35305068320c" +checksum = "1cf66c2781c36037c94a5905b6e05e7396fd4d12df09cd7f05cf96e3f0889f49" dependencies = [ "bincode", "hex", @@ -7352,9 +7518,9 @@ dependencies = [ [[package]] name = "sp1-prover" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82833ca900c54cd9f933560b433e56be97edd7322f8ec1654037e4326f3f6b7e" +checksum = "d12b04eaef751fc86d76ceb8caeeb7bcfaebc078e4d730bf265144a1bbf0cbbe" dependencies = [ "anyhow", "bincode", @@ -7395,9 +7561,9 @@ dependencies = [ [[package]] name = "sp1-recursion-circuit" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f0e78b19138ed445e02a2c69ae2115df6be3595bb464fc0e2ade550b7b9601a" +checksum = "d55bf498aed95e5244aca6fac76754d1b3013680c2813031d6060c4239b1b938" dependencies = [ "hashbrown 0.14.5", "itertools 0.13.0", @@ -7412,6 +7578,7 @@ dependencies = [ "p3-fri", "p3-matrix", "p3-symmetric", + "p3-uni-stark", "p3-util", "rand", "rayon", @@ -7429,9 +7596,9 @@ dependencies = [ [[package]] name = "sp1-recursion-compiler" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6cd484565925bc9f10af6358ebe4f8a2193af635299725294a1ee8441854689" +checksum = "7d896810412e25f4d9c96d251fac3d9f90be40f32271f213626794551d242efa" dependencies = [ "backtrace", "itertools 0.13.0", @@ -7451,9 +7618,9 @@ dependencies = [ [[package]] name = "sp1-recursion-core" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "781a49a2ade18ebafc090070a3ce62854ee3aed802fe0f36074cd9984e9183ce" +checksum = "ff5f1a78134c095d627053499a96e8314992c7b464b199c3faf35ad901789eb1" dependencies = [ "backtrace", "cbindgen", @@ -7494,9 +7661,9 @@ dependencies = [ [[package]] name = "sp1-recursion-derive" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8f0f802f8d5b1a3afca56c5b398c16a0382bef8969be8b9a3edc7b8e8b37be" +checksum = "0dbbff05801a7a22cc46575328da11e9024d7862700a918b9ef8bf761612e86e" dependencies = [ "quote", "syn 1.0.109", @@ -7504,9 +7671,9 @@ dependencies = [ [[package]] name = "sp1-recursion-gnark-ffi" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5b4480f59b320bd25beaf3b19c0d3c9b60d140a99964805b18853a611a8eaf" +checksum = "81a9de089de402f6ecab5cf65c3096cc266e02a49e59675f7a9555158217a387" dependencies = [ "anyhow", "bincode", @@ -7530,13 +7697,13 @@ dependencies = [ [[package]] name = "sp1-sdk" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47b98599a826c8d24976791495bc59c7d8be3af97a515cf5f7a7c3b02daa835" +checksum = "a42587153add9b7fdb4c0931b9e805bd6ab05e63e5837246e0b2594417c2a479" dependencies = [ "alloy-primitives", - "alloy-signer 0.8.3", - "alloy-signer-local 0.8.3", + "alloy-signer 0.11.1", + "alloy-signer-local 0.11.1", "alloy-sol-types", "anyhow", "async-trait", @@ -7573,14 +7740,13 @@ dependencies = [ "tonic", "tracing", "twirp-rs", - "vergen", ] [[package]] name = "sp1-stark" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4779a915a218d668868fd6d11f6f225e6ec0033121345772214901edc8e9fa" +checksum = "c7612e1cb9f210d15bc61fd2f9ee450246acbd5303e5475ef5fe2d2f377cc9e0" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -7613,9 +7779,9 @@ dependencies = [ [[package]] name = "sp1-verifier" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613b9d79e1dd8d1b584654710a462340ff6b4b1ad8762830c4c9f772892467f8" +checksum = "94bf3296a784f68f51caaa9d79a5dd4046c507bdcc54ca26e14c41c4ea7d844a" dependencies = [ "hex", "lazy_static", @@ -7673,7 +7839,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.2", "hashlink", - "indexmap 2.7.0", + "indexmap 2.7.1", "log", "memchr", "once_cell", @@ -7699,7 +7865,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -7722,7 +7888,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.95", + "syn 2.0.98", "tempfile", "tokio", "url", @@ -7736,7 +7902,7 @@ checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.8.0", "byteorder", "bytes", "crc", @@ -7778,7 +7944,7 @@ checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.6.0", + "bitflags 2.8.0", "byteorder", "crc", "dotenvy", @@ -7837,7 +8003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" dependencies = [ "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -7869,7 +8035,7 @@ dependencies = [ "clap", "jsonrpsee", "rand", - "secp256k1", + "secp256k1 0.29.1", "serde_json", "sqlx", "strata-bridge-agent", @@ -7896,10 +8062,10 @@ dependencies = [ "bitvm", "borsh", "jsonrpsee", - "musig2", + "musig2 0.1.0", "rand", "rkyv", - "secp256k1", + "secp256k1 0.29.1", "serde", "serde_json", "sp1-verifier", @@ -7926,9 +8092,9 @@ dependencies = [ "arbitrary", "async-trait", "bitcoin", - "musig2", + "musig2 0.1.0", "rkyv", - "secp256k1", + "secp256k1 0.29.1", "serde_json", "sqlx", "strata-bridge-primitives", @@ -7956,9 +8122,9 @@ dependencies = [ "bitcoin-bosd", "bitcoin-script", "bitvm", - "musig2", + "musig2 0.1.0", "rkyv", - "secp256k1", + "secp256k1 0.29.1", "serde", "sha2", "strata-primitives", @@ -7995,7 +8161,7 @@ dependencies = [ "strata-state", "thiserror 2.0.11", "tracing", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", "zkaleido-native-adapter", ] @@ -8028,8 +8194,8 @@ dependencies = [ "strata-primitives", "strata-state", "tracing", - "zkaleido", - "zkaleido-sp1-adapter", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", + "zkaleido-sp1-adapter 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", ] [[package]] @@ -8042,10 +8208,10 @@ dependencies = [ [[package]] name = "strata-bridge-sig-manager" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", - "musig2", + "musig2 0.1.0", "strata-db", "strata-primitives", "strata-storage", @@ -8059,7 +8225,7 @@ version = "0.1.0" dependencies = [ "bitcoin", "corepc-node", - "secp256k1", + "secp256k1 0.29.1", "serde", "strata-bridge-primitives", "strata-bridge-test-utils", @@ -8077,7 +8243,7 @@ dependencies = [ "bitcoin", "bitvm", "corepc-node", - "musig2", + "musig2 0.1.0", "rand_core", "strata-bridge-primitives", "strata-btcio", @@ -8086,10 +8252,10 @@ dependencies = [ [[package]] name = "strata-bridge-tx-builder" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", - "musig2", + "musig2 0.1.0", "serde", "strata-primitives", "thiserror 2.0.11", @@ -8107,7 +8273,7 @@ dependencies = [ "borsh", "corepc-node", "rkyv", - "secp256k1", + "secp256k1 0.29.1", "serde", "sp1-verifier", "strata-bridge-db", @@ -8125,7 +8291,7 @@ dependencies = [ [[package]] name = "strata-btcio" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "async-trait", @@ -8134,9 +8300,10 @@ dependencies = [ "bytes", "digest 0.10.7", "hex", - "musig2", + "musig2 0.1.0", "rand", "reqwest", + "secp256k1 0.29.1", "serde", "serde_json", "sha2", @@ -8159,7 +8326,7 @@ dependencies = [ [[package]] name = "strata-chaintsn" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "bitcoin", @@ -8175,7 +8342,7 @@ dependencies = [ [[package]] name = "strata-common" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", "deadpool", @@ -8193,7 +8360,7 @@ dependencies = [ [[package]] name = "strata-config" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", "serde", @@ -8202,13 +8369,14 @@ dependencies = [ [[package]] name = "strata-consensus-logic" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", + "async-trait", "bitcoin", "borsh", "futures", - "secp256k1", + "secp256k1 0.29.1", "strata-btcio", "strata-chaintsn", "strata-common", @@ -8226,17 +8394,17 @@ dependencies = [ "threadpool", "tokio", "tracing", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", "zkaleido-risc0-adapter", - "zkaleido-sp1-adapter", + "zkaleido-sp1-adapter 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] name = "strata-crypto" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ - "secp256k1", + "secp256k1 0.29.1", "sha2", "strata-primitives", ] @@ -8244,14 +8412,14 @@ dependencies = [ [[package]] name = "strata-db" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "arbitrary", "bitcoin", "borsh", "hex", - "musig2", + "musig2 0.1.0", "parking_lot", "serde", "strata-mmr", @@ -8259,13 +8427,13 @@ dependencies = [ "strata-state", "thiserror 2.0.11", "tracing", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] name = "strata-eectl" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "strata-primitives", "strata-state", @@ -8275,14 +8443,14 @@ dependencies = [ [[package]] name = "strata-l1tx" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "arbitrary", "bitcoin", "borsh", "hex", - "musig2", + "musig2 0.1.0", "strata-bridge-tx-builder", "strata-primitives", "strata-state", @@ -8293,7 +8461,7 @@ dependencies = [ [[package]] name = "strata-mmr" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "arbitrary", "borsh", @@ -8306,7 +8474,7 @@ dependencies = [ [[package]] name = "strata-primitives" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "arbitrary", @@ -8317,10 +8485,10 @@ dependencies = [ "const-hex", "digest 0.10.7", "hex", - "musig2", + "musig2 0.1.0", "num_enum 0.7.3", "rand", - "secp256k1", + "secp256k1 0.29.1", "serde", "serde_json", "sha2", @@ -8332,7 +8500,7 @@ dependencies = [ [[package]] name = "strata-proofimpl-btc-blockspace" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", "borsh", @@ -8343,13 +8511,13 @@ dependencies = [ "strata-l1tx", "strata-primitives", "strata-state", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] name = "strata-rpc-api" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", "hex", @@ -8365,13 +8533,13 @@ dependencies = [ "strata-sequencer", "strata-state", "thiserror 2.0.11", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] name = "strata-rpc-types" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", "hex", @@ -8388,7 +8556,7 @@ dependencies = [ [[package]] name = "strata-sequencer" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "borsh", @@ -8411,9 +8579,11 @@ dependencies = [ [[package]] name = "strata-state" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ + "anyhow", "arbitrary", + "async-trait", "bitcoin", "borsh", "digest 0.10.7", @@ -8425,13 +8595,13 @@ dependencies = [ "strata-crypto", "strata-primitives", "tracing", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] name = "strata-status" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "strata-primitives", "strata-rpc-types", @@ -8444,7 +8614,7 @@ dependencies = [ [[package]] name = "strata-storage" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "async-trait", @@ -8463,7 +8633,7 @@ dependencies = [ [[package]] name = "strata-tasks" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "anyhow", "futures-util", @@ -8508,7 +8678,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -8559,9 +8729,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.95" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -8570,14 +8740,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.8.18" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e89d8bf2768d277f40573c83a02a099e96d96dd3104e13ea676194e61ac4b0" +checksum = "9c2de690018098e367beeb793991c7d4dc7270f42c9d2ac4ccc876c1368ca430" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -8597,7 +8767,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -8653,18 +8823,24 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if 1.0.0", "fastrand", - "getrandom", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.59.0", ] +[[package]] +name = "terrors" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a09a4471bca83dc547eb7829f7fe0b96ad3d04d21ad155e1e14084e163fe0e07" + [[package]] name = "thiserror" version = "1.0.69" @@ -8691,7 +8867,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -8702,7 +8878,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -8732,9 +8908,7 @@ checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", - "libc", "num-conv", - "num_threads", "powerfmt", "serde", "time-core", @@ -8793,9 +8967,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -8811,13 +8985,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -8836,7 +9010,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.20", + "rustls 0.23.23", "tokio", ] @@ -8860,12 +9034,12 @@ checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", - "rustls 0.23.20", + "rustls 0.23.23", "rustls-pki-types", "tokio", "tokio-rustls", "tungstenite", - "webpki-roots 0.26.7", + "webpki-roots 0.26.8", ] [[package]] @@ -8884,14 +9058,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.22", + "toml_edit 0.22.24", ] [[package]] @@ -8909,22 +9083,22 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.7.1", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.22", + "winnow 0.7.2", ] [[package]] @@ -9051,7 +9225,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -9152,7 +9326,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.23.20", + "rustls 0.23.23", "rustls-pki-types", "sha1", "thiserror 1.0.69", @@ -9219,9 +9393,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" @@ -9293,15 +9467,15 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" @@ -9318,19 +9492,6 @@ dependencies = [ "serde", ] -[[package]] -name = "vergen" -version = "8.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" -dependencies = [ - "anyhow", - "cfg-if 1.0.0", - "git2", - "rustversion", - "time", -] - [[package]] name = "version-compare" version = "0.2.0" @@ -9345,9 +9506,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wait-timeout" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] @@ -9377,6 +9538,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.13.3+wasi-0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -9385,34 +9555,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if 1.0.0", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -9423,9 +9594,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9433,22 +9604,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-streams" @@ -9465,9 +9639,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" dependencies = [ "js-sys", "wasm-bindgen", @@ -9483,6 +9657,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.25.4" @@ -9491,9 +9674,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" dependencies = [ "rustls-pki-types", ] @@ -9777,13 +9960,22 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.22" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" dependencies = [ "memchr", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "write16" version = "1.0.0" @@ -9865,7 +10057,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", "synstructure", ] @@ -9887,7 +10079,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -9907,7 +10099,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", "synstructure", ] @@ -9928,7 +10120,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -9960,7 +10152,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.95", + "syn 2.0.98", ] [[package]] @@ -9995,6 +10187,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "zkaleido" +version = "0.1.0" +source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2#737d8e591be90d84ab7639d8bf1bb2ed36f2735f" +dependencies = [ + "arbitrary", + "bincode", + "borsh", + "serde", + "thiserror 1.0.69", +] + [[package]] name = "zkaleido-native-adapter" version = "0.1.0" @@ -10004,22 +10208,18 @@ dependencies = [ "borsh", "serde", "tracing", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", ] [[package]] name = "zkaleido-risc0-adapter" version = "0.1.0" -source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1#132da748d0789cd5799d5f949d017141926802e7" +source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2#737d8e591be90d84ab7639d8bf1bb2ed36f2735f" dependencies = [ - "bincode", - "borsh", - "hex", "risc0-zkvm", "serde", "sha2", - "tracing-subscriber 0.3.19", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] @@ -10036,7 +10236,17 @@ dependencies = [ "sp1-sdk", "sp1-verifier", "tracing", - "zkaleido", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", +] + +[[package]] +name = "zkaleido-sp1-adapter" +version = "0.1.0" +source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2#737d8e591be90d84ab7639d8bf1bb2ed36f2735f" +dependencies = [ + "hex", + "sp1-verifier", + "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", ] [[package]] @@ -10134,3 +10344,8 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "ark-std" +version = "0.5.0" +source = "git+https://github.com/arkworks-rs/std/#b0cbdeb097b42f61880b3bee19e8dd37258d23a5" diff --git a/Cargo.toml b/Cargo.toml index c296de7b..43fca9ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,10 @@ members = [ # test utilities "crates/test-utils", + "crates/mi6", + "crates/mi6-proto", + "crates/mi6-client", + "crates/mi6-server", ] default-members = ["bin/strata-bridge", "bin/dev-cli", "bin/assert-splitter"] @@ -90,10 +94,12 @@ futures = "0.3.31" hex = { version = "0.4", features = ["serde"] } jsonrpsee = "0.24.7" jsonrpsee-types = "0.24.7" +kanal = "0.1.0-pre8" musig2 = { version = "0.1.0", features = [ "serde", "rand", ] } # can't be updated without updating bitcoin +quinn = "0.11.6" rand = "0.8.5" reqwest = { version = "0.12.12", default-features = false, features = [ "http2", @@ -120,6 +126,7 @@ sqlx = { version = "0.8.2", features = [ "migrate", ] } tempfile = "3.10.1" +terrors = "0.3.2" thiserror = "2.0.3" tokio = { version = "1.37", features = ["full"] } tracing = "0.1.40" diff --git a/crates/mi6-client/Cargo.toml b/crates/mi6-client/Cargo.toml new file mode 100644 index 00000000..83a5535a --- /dev/null +++ b/crates/mi6-client/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mi6-client" +version = "0.1.0" +edition = "2021" + +[dependencies] +kanal.workspace = true +mi6-proto = { version = "0.1.0", path = "../mi6-proto" } +quinn.workspace = true +rkyv.workspace = true +terrors.workspace = true +tokio.workspace = true +tracing.workspace = true diff --git a/crates/mi6-client/src/lib.rs b/crates/mi6-client/src/lib.rs new file mode 100644 index 00000000..d85c8854 --- /dev/null +++ b/crates/mi6-client/src/lib.rs @@ -0,0 +1,65 @@ +use std::{ + io, + net::{Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use quinn::{ + crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, + rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, +}; +use terrors::OneOf; + +#[derive(Clone)] +pub struct Config { + server_addr: SocketAddr, + server_hostname: String, + local_addr: Option, + connection_limit: Option, + tls_config: rustls::ClientConfig, +} + +#[derive(Clone)] +pub struct Client { + endpoint: Endpoint, + config: Config, + conn: Option, +} + +impl Client { + pub fn new(config: Config) -> Result { + let endpoint = Endpoint::client( + config + .local_addr + .unwrap_or((Ipv4Addr::UNSPECIFIED, 0).into()), + )?; + Ok(Client { + endpoint, + config, + conn: None, + }) + } + + pub async fn connect( + &mut self, + ) -> Result<(), OneOf<(NoInitialCipherSuite, ConnectError, ConnectionError)>> { + if self.conn.is_some() { + return Ok(()); + } + + let connecting = self + .endpoint + .connect_with( + ClientConfig::new(Arc::new( + QuicClientConfig::try_from(self.config.tls_config.clone()) + .map_err(OneOf::new)?, + )), + self.config.server_addr, + &self.config.server_hostname, + ) + .map_err(OneOf::new)?; + let conn = connecting.await.map_err(OneOf::new)?; + self.conn = Some(conn); + Ok(()) + } +} diff --git a/crates/mi6-proto/Cargo.toml b/crates/mi6-proto/Cargo.toml new file mode 100644 index 00000000..1074d2b8 --- /dev/null +++ b/crates/mi6-proto/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "mi6-proto" +version = "0.1.0" +edition = "2021" + +[dependencies] +rkyv.workspace = true diff --git a/crates/mi6-proto/src/lib.rs b/crates/mi6-proto/src/lib.rs new file mode 100644 index 00000000..f89819b6 --- /dev/null +++ b/crates/mi6-proto/src/lib.rs @@ -0,0 +1,17 @@ +pub mod wire; +pub mod traits; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/crates/mi6-proto/src/traits.rs b/crates/mi6-proto/src/traits.rs new file mode 100644 index 00000000..a935c30d --- /dev/null +++ b/crates/mi6-proto/src/traits.rs @@ -0,0 +1,5 @@ +pub trait MI6Factory { + fn produce(&self) -> Box; +} + +pub trait MI6 {} diff --git a/crates/mi6-proto/src/wire.rs b/crates/mi6-proto/src/wire.rs new file mode 100644 index 00000000..446ab622 --- /dev/null +++ b/crates/mi6-proto/src/wire.rs @@ -0,0 +1,47 @@ +use rkyv::{ + api::high::{to_bytes_in, HighSerializer}, + rancor, + ser::allocator::ArenaHandle, + util::AlignedVec, + Archive, Deserialize, Serialize, +}; + +trait WireMessageMarker: + for<'a> Serialize, rancor::Error>> +{ +} + +#[derive(Archive, Serialize, Deserialize)] +pub enum ServerMessage { + Bob, +} + +impl WireMessageMarker for ServerMessage {} + +#[derive(Archive, Serialize, Deserialize)] +pub enum ClientMessage { + Bob, +} + +impl WireMessageMarker for ClientMessage {} + +pub trait WireMessage { + fn serialize(&self) -> Result; +} + +// ignore, probably will just directly write to the connection instead of this +impl WireMessage for T { + fn serialize(&self) -> Result { + let mut aligned_buf = AlignedVec::new(); + // write a default length + aligned_buf.extend_from_slice(&u32::MAX.to_le_bytes()); + let mut aligned_buf = to_bytes_in(self, aligned_buf)?; + let len = aligned_buf.len() - 4; + debug_assert!(len <= u32::MAX as usize); + let len_as_le_bytes = (len as u32).to_le_bytes(); + for i in 0..4 { + aligned_buf[i] = len_as_le_bytes[i] + } + Ok(aligned_buf) + } +} diff --git a/crates/mi6-server/Cargo.toml b/crates/mi6-server/Cargo.toml new file mode 100644 index 00000000..35d41c80 --- /dev/null +++ b/crates/mi6-server/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mi6-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +kanal.workspace = true +mi6-proto = { version = "0.1.0", path = "../mi6-proto" } +quinn.workspace = true +rkyv.workspace = true +terrors.workspace = true +tokio.workspace = true +tracing.workspace = true diff --git a/crates/mi6-server/src/lib.rs b/crates/mi6-server/src/lib.rs new file mode 100644 index 00000000..23ea2d00 --- /dev/null +++ b/crates/mi6-server/src/lib.rs @@ -0,0 +1,52 @@ +use std::{future::Future, io, net::SocketAddr, pin::Pin, sync::Arc}; + +use mi6_proto::traits::MI6Factory; +use quinn::{ + crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, + rustls, Endpoint, ServerConfig, +}; +use terrors::OneOf; +use tokio::task::{JoinError, JoinHandle}; + +pub struct Config { + addr: SocketAddr, + connection_limit: Option, + tls_config: rustls::ServerConfig, +} + +pub struct ServerHandle { + main: JoinHandle<()>, +} + +impl Future for ServerHandle { + type Output = Result<(), JoinError>; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + Pin::new(&mut self.get_mut().main).poll(cx) + } +} + +pub fn run_server( + c: Config, + factory: Factory, +) -> Result> { + let quic_server_config = ServerConfig::with_crypto(Arc::new( + QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, + )); + let endpoint = Endpoint::server(quic_server_config, c.addr).map_err(OneOf::new)?; + let handle = tokio::spawn(async move { + while let Some(conn) = endpoint.accept().await { + if c.connection_limit + .is_some_and(|n| endpoint.open_connections() >= n) + { + conn.refuse(); + } else { + // handle_conn(conn, factory).await; + } + } + }); + Ok(ServerHandle { main: handle }) +} diff --git a/crates/mi6/Cargo.toml b/crates/mi6/Cargo.toml new file mode 100644 index 00000000..3d2c4df0 --- /dev/null +++ b/crates/mi6/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mi6" +version = "0.1.0" +edition = "2021" + +[dependencies] +bdk_wallet = "1.0.0" +mi6-proto = { version = "0.1.0", path = "../mi6-proto" } +musig2 = "0.2" + +tokio.workspace = true diff --git a/crates/mi6/src/main.rs b/crates/mi6/src/main.rs new file mode 100644 index 00000000..7e3d561d --- /dev/null +++ b/crates/mi6/src/main.rs @@ -0,0 +1,4 @@ +#[tokio::main] +async fn main() { + println!("Hello, world!"); +} From 5f8d306fbe42e152635e289e1ae069766b62f699 Mon Sep 17 00:00:00 2001 From: Azz Date: Fri, 24 Jan 2025 15:34:16 +0000 Subject: [PATCH 02/30] init --- Cargo.toml | 10 +- crates/mi6-client/src/lib.rs | 65 --- crates/mi6-proto/src/traits.rs | 5 - crates/mi6-proto/src/wire.rs | 47 -- crates/mi6-server/Cargo.toml | 13 - crates/mi6-server/src/lib.rs | 52 -- crates/mi6/Cargo.toml | 11 - crates/mi6/src/main.rs | 4 - .../Cargo.toml | 5 +- crates/secret-service-client/src/lib.rs | 184 +++++++ .../Cargo.toml | 4 +- .../src/lib.rs | 2 +- crates/secret-service-proto/src/v1/mod.rs | 3 + .../src/v1/rkyv_wrappers.rs | 154 ++++++ crates/secret-service-proto/src/v1/traits.rs | 132 +++++ crates/secret-service-proto/src/v1/wire.rs | 165 ++++++ crates/secret-service-proto/src/wire.rs | 9 + crates/secret-service-server/Cargo.toml | 17 + crates/secret-service-server/src/bool_arr.rs | 69 +++ crates/secret-service-server/src/lib.rs | 492 ++++++++++++++++++ crates/secret-service-server/src/ms2sm.rs | 207 ++++++++ crates/secret-service/Cargo.toml | 14 + crates/secret-service/src/main.rs | 8 + 23 files changed, 1467 insertions(+), 205 deletions(-) delete mode 100644 crates/mi6-client/src/lib.rs delete mode 100644 crates/mi6-proto/src/traits.rs delete mode 100644 crates/mi6-proto/src/wire.rs delete mode 100644 crates/mi6-server/Cargo.toml delete mode 100644 crates/mi6-server/src/lib.rs delete mode 100644 crates/mi6/Cargo.toml delete mode 100644 crates/mi6/src/main.rs rename crates/{mi6-client => secret-service-client}/Cargo.toml (60%) create mode 100644 crates/secret-service-client/src/lib.rs rename crates/{mi6-proto => secret-service-proto}/Cargo.toml (51%) rename crates/{mi6-proto => secret-service-proto}/src/lib.rs (93%) create mode 100644 crates/secret-service-proto/src/v1/mod.rs create mode 100644 crates/secret-service-proto/src/v1/rkyv_wrappers.rs create mode 100644 crates/secret-service-proto/src/v1/traits.rs create mode 100644 crates/secret-service-proto/src/v1/wire.rs create mode 100644 crates/secret-service-proto/src/wire.rs create mode 100644 crates/secret-service-server/Cargo.toml create mode 100644 crates/secret-service-server/src/bool_arr.rs create mode 100644 crates/secret-service-server/src/lib.rs create mode 100644 crates/secret-service-server/src/ms2sm.rs create mode 100644 crates/secret-service/Cargo.toml create mode 100644 crates/secret-service/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 43fca9ab..6244526a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,10 +20,10 @@ members = [ # test utilities "crates/test-utils", - "crates/mi6", - "crates/mi6-proto", - "crates/mi6-client", - "crates/mi6-server", + "crates/secret-service", + "crates/secret-service-proto", + "crates/secret-service-client", + "crates/secret-service-server", ] default-members = ["bin/strata-bridge", "bin/dev-cli", "bin/assert-splitter"] @@ -80,6 +80,7 @@ bitcoin = { version = "0.32.5", features = ["rand-std", "serde"] } bitcoin-bosd = "0.4.0" bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" } bitvm = { git = "https://github.com/alpenlabs/BitVM.git", branch = "testnet-i" } +blake3 = { version = "1.5.5", features = ["zeroize"] } borsh = { version = "1.5.0", features = ["derive"] } chrono = "0.4.38" clap = { version = "4.5.20", features = ["cargo", "derive", "env"] } @@ -99,6 +100,7 @@ musig2 = { version = "0.1.0", features = [ "serde", "rand", ] } # can't be updated without updating bitcoin +parking_lot = "0.12.3" quinn = "0.11.6" rand = "0.8.5" reqwest = { version = "0.12.12", default-features = false, features = [ diff --git a/crates/mi6-client/src/lib.rs b/crates/mi6-client/src/lib.rs deleted file mode 100644 index d85c8854..00000000 --- a/crates/mi6-client/src/lib.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::{ - io, - net::{Ipv4Addr, SocketAddr}, - sync::Arc, -}; - -use quinn::{ - crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, - rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, -}; -use terrors::OneOf; - -#[derive(Clone)] -pub struct Config { - server_addr: SocketAddr, - server_hostname: String, - local_addr: Option, - connection_limit: Option, - tls_config: rustls::ClientConfig, -} - -#[derive(Clone)] -pub struct Client { - endpoint: Endpoint, - config: Config, - conn: Option, -} - -impl Client { - pub fn new(config: Config) -> Result { - let endpoint = Endpoint::client( - config - .local_addr - .unwrap_or((Ipv4Addr::UNSPECIFIED, 0).into()), - )?; - Ok(Client { - endpoint, - config, - conn: None, - }) - } - - pub async fn connect( - &mut self, - ) -> Result<(), OneOf<(NoInitialCipherSuite, ConnectError, ConnectionError)>> { - if self.conn.is_some() { - return Ok(()); - } - - let connecting = self - .endpoint - .connect_with( - ClientConfig::new(Arc::new( - QuicClientConfig::try_from(self.config.tls_config.clone()) - .map_err(OneOf::new)?, - )), - self.config.server_addr, - &self.config.server_hostname, - ) - .map_err(OneOf::new)?; - let conn = connecting.await.map_err(OneOf::new)?; - self.conn = Some(conn); - Ok(()) - } -} diff --git a/crates/mi6-proto/src/traits.rs b/crates/mi6-proto/src/traits.rs deleted file mode 100644 index a935c30d..00000000 --- a/crates/mi6-proto/src/traits.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub trait MI6Factory { - fn produce(&self) -> Box; -} - -pub trait MI6 {} diff --git a/crates/mi6-proto/src/wire.rs b/crates/mi6-proto/src/wire.rs deleted file mode 100644 index 446ab622..00000000 --- a/crates/mi6-proto/src/wire.rs +++ /dev/null @@ -1,47 +0,0 @@ -use rkyv::{ - api::high::{to_bytes_in, HighSerializer}, - rancor, - ser::allocator::ArenaHandle, - util::AlignedVec, - Archive, Deserialize, Serialize, -}; - -trait WireMessageMarker: - for<'a> Serialize, rancor::Error>> -{ -} - -#[derive(Archive, Serialize, Deserialize)] -pub enum ServerMessage { - Bob, -} - -impl WireMessageMarker for ServerMessage {} - -#[derive(Archive, Serialize, Deserialize)] -pub enum ClientMessage { - Bob, -} - -impl WireMessageMarker for ClientMessage {} - -pub trait WireMessage { - fn serialize(&self) -> Result; -} - -// ignore, probably will just directly write to the connection instead of this -impl WireMessage for T { - fn serialize(&self) -> Result { - let mut aligned_buf = AlignedVec::new(); - // write a default length - aligned_buf.extend_from_slice(&u32::MAX.to_le_bytes()); - let mut aligned_buf = to_bytes_in(self, aligned_buf)?; - let len = aligned_buf.len() - 4; - debug_assert!(len <= u32::MAX as usize); - let len_as_le_bytes = (len as u32).to_le_bytes(); - for i in 0..4 { - aligned_buf[i] = len_as_le_bytes[i] - } - Ok(aligned_buf) - } -} diff --git a/crates/mi6-server/Cargo.toml b/crates/mi6-server/Cargo.toml deleted file mode 100644 index 35d41c80..00000000 --- a/crates/mi6-server/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "mi6-server" -version = "0.1.0" -edition = "2021" - -[dependencies] -kanal.workspace = true -mi6-proto = { version = "0.1.0", path = "../mi6-proto" } -quinn.workspace = true -rkyv.workspace = true -terrors.workspace = true -tokio.workspace = true -tracing.workspace = true diff --git a/crates/mi6-server/src/lib.rs b/crates/mi6-server/src/lib.rs deleted file mode 100644 index 23ea2d00..00000000 --- a/crates/mi6-server/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::{future::Future, io, net::SocketAddr, pin::Pin, sync::Arc}; - -use mi6_proto::traits::MI6Factory; -use quinn::{ - crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, - rustls, Endpoint, ServerConfig, -}; -use terrors::OneOf; -use tokio::task::{JoinError, JoinHandle}; - -pub struct Config { - addr: SocketAddr, - connection_limit: Option, - tls_config: rustls::ServerConfig, -} - -pub struct ServerHandle { - main: JoinHandle<()>, -} - -impl Future for ServerHandle { - type Output = Result<(), JoinError>; - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll { - Pin::new(&mut self.get_mut().main).poll(cx) - } -} - -pub fn run_server( - c: Config, - factory: Factory, -) -> Result> { - let quic_server_config = ServerConfig::with_crypto(Arc::new( - QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, - )); - let endpoint = Endpoint::server(quic_server_config, c.addr).map_err(OneOf::new)?; - let handle = tokio::spawn(async move { - while let Some(conn) = endpoint.accept().await { - if c.connection_limit - .is_some_and(|n| endpoint.open_connections() >= n) - { - conn.refuse(); - } else { - // handle_conn(conn, factory).await; - } - } - }); - Ok(ServerHandle { main: handle }) -} diff --git a/crates/mi6/Cargo.toml b/crates/mi6/Cargo.toml deleted file mode 100644 index 3d2c4df0..00000000 --- a/crates/mi6/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "mi6" -version = "0.1.0" -edition = "2021" - -[dependencies] -bdk_wallet = "1.0.0" -mi6-proto = { version = "0.1.0", path = "../mi6-proto" } -musig2 = "0.2" - -tokio.workspace = true diff --git a/crates/mi6/src/main.rs b/crates/mi6/src/main.rs deleted file mode 100644 index 7e3d561d..00000000 --- a/crates/mi6/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[tokio::main] -async fn main() { - println!("Hello, world!"); -} diff --git a/crates/mi6-client/Cargo.toml b/crates/secret-service-client/Cargo.toml similarity index 60% rename from crates/mi6-client/Cargo.toml rename to crates/secret-service-client/Cargo.toml index 83a5535a..3bf9d25a 100644 --- a/crates/mi6-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -1,13 +1,14 @@ [package] -name = "mi6-client" +name = "secret-service-client" version = "0.1.0" edition = "2021" [dependencies] kanal.workspace = true -mi6-proto = { version = "0.1.0", path = "../mi6-proto" } +musig2.workspace = true quinn.workspace = true rkyv.workspace = true +secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } terrors.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs new file mode 100644 index 00000000..010e8a8b --- /dev/null +++ b/crates/secret-service-client/src/lib.rs @@ -0,0 +1,184 @@ +use std::{ + future::Future, + io, + net::{Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use musig2::{ + errors::{RoundContributionError, RoundFinalizeError}, + secp256k1::PublicKey, + PubNonce, +}; +use quinn::{ + crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, + rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, +}; +use secret_service_proto::v1::traits::{ + self, Client, Musig2SessionId, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, + SecretService, +}; +use terrors::OneOf; + +#[derive(Clone)] +pub struct Config { + server_addr: SocketAddr, + server_hostname: String, + local_addr: Option, + connection_limit: Option, + tls_config: rustls::ClientConfig, +} + +#[derive(Clone)] +pub struct SecretServiceClient { + endpoint: Endpoint, + config: Config, + conn: Option, +} + +impl SecretServiceClient { + pub fn new(config: Config) -> Result { + let endpoint = Endpoint::client( + config + .local_addr + .unwrap_or((Ipv4Addr::UNSPECIFIED, 0).into()), + )?; + Ok(SecretServiceClient { + endpoint, + config, + conn: None, + }) + } + + pub async fn connect( + &mut self, + ) -> Result<(), OneOf<(NoInitialCipherSuite, ConnectError, ConnectionError)>> { + if self.conn.is_some() { + return Ok(()); + } + + let connecting = self + .endpoint + .connect_with( + ClientConfig::new(Arc::new( + QuicClientConfig::try_from(self.config.tls_config.clone()) + .map_err(OneOf::new)?, + )), + self.config.server_addr, + &self.config.server_hostname, + ) + .map_err(OneOf::new)?; + let conn = connecting.await.map_err(OneOf::new)?; + self.conn = Some(conn); + Ok(()) + } +} + +struct Musig2FirstRound { + session_id: Musig2SessionId, +} + +impl Musig2SignerFirstRound for Musig2FirstRound { + fn our_nonce(&self) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send { + async move { todo!() } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn receive_pub_nonce( + &self, + pubkey: PublicKey, + pubnonce: PubNonce, + ) -> impl Future::Container>> + Send + { + async move { todo!() } + } + + fn finalize( + self, + hash: [u8; 32], + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { todo!() } + } +} + +struct Musig2SecondRound { + session_id: Musig2SessionId, +} + +impl Musig2SignerSecondRound for Musig2SecondRound { + fn agg_nonce( + &self, + ) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send { + async move { todo!() } + } + + fn our_signature( + &self, + ) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn receive_signature( + &self, + pubkey: PublicKey, + signature: musig2::PartialSignature, + ) -> impl Future::Container>> + Send + { + async move { todo!() } + } + + fn finalize( + self, + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { todo!() } + } +} + +// impl SecretService for SecretServiceClient { +// type OperatorSigner; + +// type P2PSigner; + +// type Musig2Signer; + +// type WotsSigner; + +// fn operator_signer(&self) -> &Self::OperatorSigner { +// todo!() +// } + +// fn p2p_signer(&self) -> &Self::P2PSigner { +// todo!() +// } + +// fn musig2_signer(&self) -> &Self::Musig2Signer { +// todo!() +// } + +// fn wots_signer(&self) -> &Self::WotsSigner { +// todo!() +// } +// } diff --git a/crates/mi6-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml similarity index 51% rename from crates/mi6-proto/Cargo.toml rename to crates/secret-service-proto/Cargo.toml index 1074d2b8..837a87ac 100644 --- a/crates/mi6-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -1,7 +1,9 @@ [package] -name = "mi6-proto" +name = "secret-service-proto" version = "0.1.0" edition = "2021" [dependencies] +bitcoin.workspace = true +musig2.workspace = true rkyv.workspace = true diff --git a/crates/mi6-proto/src/lib.rs b/crates/secret-service-proto/src/lib.rs similarity index 93% rename from crates/mi6-proto/src/lib.rs rename to crates/secret-service-proto/src/lib.rs index f89819b6..b6c97292 100644 --- a/crates/mi6-proto/src/lib.rs +++ b/crates/secret-service-proto/src/lib.rs @@ -1,5 +1,5 @@ +pub mod v1; pub mod wire; -pub mod traits; pub fn add(left: u64, right: u64) -> u64 { left + right diff --git a/crates/secret-service-proto/src/v1/mod.rs b/crates/secret-service-proto/src/v1/mod.rs new file mode 100644 index 00000000..ecb52929 --- /dev/null +++ b/crates/secret-service-proto/src/v1/mod.rs @@ -0,0 +1,3 @@ +pub mod rkyv_wrappers; +pub mod traits; +pub mod wire; diff --git a/crates/secret-service-proto/src/v1/rkyv_wrappers.rs b/crates/secret-service-proto/src/v1/rkyv_wrappers.rs new file mode 100644 index 00000000..446935e0 --- /dev/null +++ b/crates/secret-service-proto/src/v1/rkyv_wrappers.rs @@ -0,0 +1,154 @@ +//! This module contains rkyv wrappers for various remote types. +//! +//! These are not intended to be used directly and therefore have no documentation. +//! +//! These are intended to be used with `#[rkyv(with = ...)]` to allow rkyv to serialize and +//! deserialize these remote types. + +use rkyv::{Archive, Deserialize, Serialize}; + +#[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = musig2::errors::RoundContributionError)] +#[rkyv(archived = ArchivedRoundContributionError)] +pub struct RoundContributionError { + pub index: usize, + + #[rkyv(with = ContributionFaultReason)] + pub reason: musig2::errors::ContributionFaultReason, +} + +impl From for RoundContributionError { + fn from(value: musig2::errors::RoundContributionError) -> Self { + Self { + index: value.index, + reason: value.reason, + } + } +} + +impl From for musig2::errors::RoundContributionError { + fn from(value: RoundContributionError) -> Self { + Self { + index: value.index, + reason: value.reason, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = musig2::errors::ContributionFaultReason)] +#[rkyv(archived = ArchivedContributionFaultReason)] +pub enum ContributionFaultReason { + OutOfRange(usize), + InconsistentContribution, + InvalidSignature, +} + +impl From for ContributionFaultReason { + fn from(value: musig2::errors::ContributionFaultReason) -> Self { + match value { + musig2::errors::ContributionFaultReason::OutOfRange(v) => Self::OutOfRange(v), + musig2::errors::ContributionFaultReason::InconsistentContribution => { + Self::InconsistentContribution + } + musig2::errors::ContributionFaultReason::InvalidSignature => Self::InvalidSignature, + } + } +} + +impl From for musig2::errors::ContributionFaultReason { + fn from(value: ContributionFaultReason) -> Self { + match value { + ContributionFaultReason::OutOfRange(v) => Self::OutOfRange(v), + ContributionFaultReason::InconsistentContribution => Self::InconsistentContribution, + ContributionFaultReason::InvalidSignature => Self::InvalidSignature, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = musig2::errors::RoundFinalizeError)] +#[rkyv(archived = ArchivedRoundFinalizeError)] +pub enum RoundFinalizeError { + Incomplete, + SigningError(#[rkyv(with = SigningError)] musig2::errors::SigningError), + InvalidAggregatedSignature(#[rkyv(with = VerifyError)] musig2::errors::VerifyError), +} + +impl From for RoundFinalizeError { + fn from(value: musig2::errors::RoundFinalizeError) -> Self { + match value { + musig2::errors::RoundFinalizeError::Incomplete => Self::Incomplete, + musig2::errors::RoundFinalizeError::SigningError(v) => Self::SigningError(v), + musig2::errors::RoundFinalizeError::InvalidAggregatedSignature(v) => { + Self::InvalidAggregatedSignature(v) + } + } + } +} + +impl From for musig2::errors::RoundFinalizeError { + fn from(value: RoundFinalizeError) -> Self { + match value { + RoundFinalizeError::Incomplete => musig2::errors::RoundFinalizeError::Incomplete, + RoundFinalizeError::SigningError(v) => { + musig2::errors::RoundFinalizeError::SigningError(v) + } + RoundFinalizeError::InvalidAggregatedSignature(v) => { + musig2::errors::RoundFinalizeError::InvalidAggregatedSignature(v) + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = musig2::errors::SigningError)] +#[rkyv(archived = ArchivedSigningError)] +pub enum SigningError { + UnknownKey, + SelfVerifyFail, +} + +impl From for SigningError { + fn from(value: musig2::errors::SigningError) -> Self { + match value { + musig2::errors::SigningError::UnknownKey => Self::UnknownKey, + musig2::errors::SigningError::SelfVerifyFail => Self::SelfVerifyFail, + } + } +} + +impl From for musig2::errors::SigningError { + fn from(value: SigningError) -> Self { + match value { + SigningError::UnknownKey => musig2::errors::SigningError::UnknownKey, + SigningError::SelfVerifyFail => musig2::errors::SigningError::SelfVerifyFail, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = musig2::errors::VerifyError)] +#[rkyv(archived = ArchivedVerifyError)] +pub enum VerifyError { + UnknownKey, + BadSignature, +} + +impl From for VerifyError { + fn from(value: musig2::errors::VerifyError) -> Self { + match value { + musig2::errors::VerifyError::UnknownKey => Self::UnknownKey, + musig2::errors::VerifyError::BadSignature => Self::BadSignature, + } + } +} + +impl From for musig2::errors::VerifyError { + fn from(value: VerifyError) -> Self { + match value { + VerifyError::UnknownKey => musig2::errors::VerifyError::UnknownKey, + VerifyError::BadSignature => musig2::errors::VerifyError::BadSignature, + } + } +} diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs new file mode 100644 index 00000000..39a5c35d --- /dev/null +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -0,0 +1,132 @@ +use std::{fmt::Debug, future::Future}; + +use bitcoin::Psbt; +use musig2::{ + errors::{RoundContributionError, RoundFinalizeError}, + secp256k1::PublicKey, + AggNonce, LiftedSignature, PartialSignature, PubNonce, +}; +use rkyv::{ + api::high::HighSerializer, rancor, ser::allocator::ArenaHandle, util::AlignedVec, Serialize, +}; + +pub trait SecretServiceFactory: Send + Clone +where + FirstRound: Musig2SignerFirstRound, + SecondRound: Musig2SignerSecondRound, +{ + type Context: Send + Clone; + type Service: SecretService + Send; + fn produce(ctx: Self::Context) -> Self::Service; +} + +// possible when https://github.com/rust-lang/rust/issues/63063 is stabliized +// pub type AsyncResult = impl Future>; + +pub trait SecretService: Send +where + O: Origin, + FirstRound: Musig2SignerFirstRound, +{ + type OperatorSigner: OperatorSigner; + type P2PSigner: P2PSigner; + type Musig2Signer: Musig2Signer; + type WotsSigner: WotsSigner; + + fn operator_signer(&self) -> &Self::OperatorSigner; + fn p2p_signer(&self) -> &Self::P2PSigner; + fn musig2_signer(&self) -> &Self::Musig2Signer; + fn wots_signer(&self) -> &Self::WotsSigner; +} + +pub trait OperatorSigner: Send { + type OperatorSigningError: Debug + + Send + + Clone + + for<'a> Serialize, rancor::Error>>; + + fn sign_psbt( + &self, + psbt: Psbt, + ) -> impl Future>> + Send; +} + +pub trait P2PSigner: Send { + type P2PSigningError: Debug + + Send + + Clone + + for<'a> Serialize, rancor::Error>>; + + fn sign_p2p( + &self, + hash: [u8; 32], + ) -> impl Future>> + Send; + + fn p2p_pubkey(&self) -> impl Future + Send; +} + +pub type Musig2SessionId = usize; + +pub trait Musig2Signer: Send + Sync { + fn new_session(&self) -> impl Future> + Send; +} + +pub trait Musig2SignerFirstRound: Send + Sync { + fn our_nonce(&self) -> impl Future> + Send; + + fn holdouts(&self) -> impl Future>> + Send; + + fn is_complete(&self) -> impl Future> + Send; + + fn receive_pub_nonce( + &self, + pubkey: PublicKey, + pubnonce: PubNonce, + ) -> impl Future>> + Send; + + fn finalize( + self, + hash: [u8; 32], + ) -> impl Future>> + Send; +} + +pub trait Musig2SignerSecondRound: Send + Sync { + fn agg_nonce(&self) -> impl Future> + Send; + + fn holdouts(&self) -> impl Future>> + Send; + + fn our_signature(&self) -> impl Future> + Send; + + fn is_complete(&self) -> impl Future> + Send; + + fn receive_signature( + &self, + pubkey: PublicKey, + signature: PartialSignature, + ) -> impl Future>> + Send; + + fn finalize( + self, + ) -> impl Future>> + Send; +} + +pub trait WotsSigner: Send { + fn get_key(&self, index: u64) -> impl Future> + Send; +} + +pub trait Origin { + type Container; +} + +/// Enforcer for other traits to ensure implementations only work for either the client or server +pub struct Server; +impl Origin for Server { + type Container = T; +} + +pub struct Client; +impl Origin for Client { + type Container = Result; +} + +pub enum NetworkError {} diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs new file mode 100644 index 00000000..0dbf923a --- /dev/null +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -0,0 +1,165 @@ +use musig2::errors::{RoundContributionError, RoundFinalizeError}; +use rkyv::{ + api::high::{to_bytes_in, HighSerializer}, + rancor, + ser::allocator::ArenaHandle, + util::AlignedVec, + with::Map, + Archive, Deserialize, Serialize, +}; + +use super::traits::{ + Musig2SessionId, Musig2SignerFirstRound, OperatorSigner, P2PSigner, SecretService, Server, +}; + +trait WireMessageMarker: + for<'a> Serialize, rancor::Error>> +{ +} + +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum ServerMessage +where + S: SecretService, + FirstRound: Musig2SignerFirstRound, +{ + InvalidClientMessage, + OpaqueServerError, + + OperatorSignPsbt( + Result, >::OperatorSigningError>, + ), + + SignP2P(Result<[u8; 64], >::P2PSigningError>), + + Musig2NewSession(Musig2SessionId), + + Musig2FirstRoundOurNonce([u8; 66]), + Musig2FirstRoundHoldouts(Vec<[u8; 33]>), + Musig2FirstRoundIsComplete(bool), + Musig2FirstRoundReceivePubNonce( + #[rkyv(with = Map)] + Option, + ), + Musig2FirstRoundFinalize( + #[rkyv(with = Map)] Option, + ), + + Musig2SecondRoundAggNonce([u8; 66]), + Musig2SecondRoundHoldouts(Vec<[u8; 33]>), + Musig2SecondRoundOurSignature([u8; 32]), + Musig2SecondRoundIsComplete(bool), + Musig2SecondRoundReceiveSignature( + #[rkyv(with = Map)] + Option, + ), + Musig2SecondRoundFinalize(Musig2SessionResult), + + WotsGetKey([u8; 64]), +} + +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum Musig2SessionResult { + Ok([u8; 64]), + Err(#[rkyv(with = super::rkyv_wrappers::RoundFinalizeError)] RoundFinalizeError), +} + +impl From> for Musig2SessionResult { + fn from(value: Result<[u8; 64], RoundFinalizeError>) -> Self { + match value { + Ok(v) => Self::Ok(v), + Err(v) => Self::Err(v), + } + } +} + +impl From for Result<[u8; 64], RoundFinalizeError> { + fn from(value: Musig2SessionResult) -> Self { + match value { + Musig2SessionResult::Ok(v) => Ok(v), + Musig2SessionResult::Err(v) => Err(v), + } + } +} + +// impl> WireMessageMarker for ServerMessage {} + +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum ClientMessage { + OperatorSignPsbt { + psbt: Vec, + }, + + SignP2P { + hash: [u8; 32], + }, + + Musig2NewSession, + + Musig2FirstRoundOurNonce { + session_id: usize, + }, + Musig2FirstRoundHoldouts { + session_id: usize, + }, + Musig2FirstRoundIsComplete { + session_id: usize, + }, + Musig2FirstRoundReceivePubNonce { + session_id: usize, + pubkey: [u8; 33], + pubnonce: [u8; 66], + }, + Musig2FirstRoundFinalize { + session_id: usize, + hash: [u8; 32], + }, + + Musig2SecondRoundAggNonce { + session_id: usize, + }, + Musig2SecondRoundHoldouts { + session_id: usize, + }, + Musig2SecondRoundOurSignature { + session_id: usize, + }, + Musig2SecondRoundIsComplete { + session_id: usize, + }, + Musig2SecondRoundReceiveSignature { + session_id: usize, + pubkey: [u8; 33], + signature: [u8; 32], + }, + Musig2SecondRoundFinalize { + session_id: usize, + }, + + WotsGetKey { + index: u64, + }, +} + +impl WireMessageMarker for ClientMessage {} + +pub trait WireMessage { + fn serialize(&self) -> Result; +} + +// ignore, probably will just directly write to the connection instead of this +impl WireMessage for T { + fn serialize(&self) -> Result { + let mut aligned_buf = AlignedVec::new(); + // write a default length + aligned_buf.extend_from_slice(&u32::MAX.to_le_bytes()); + let mut aligned_buf = to_bytes_in(self, aligned_buf)?; + let len = aligned_buf.len() - 4; + debug_assert!(len <= u32::MAX as usize); + let len_as_le_bytes = (len as u32).to_le_bytes(); + for i in 0..4 { + aligned_buf[i] = len_as_le_bytes[i] + } + Ok(aligned_buf) + } +} diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs new file mode 100644 index 00000000..bf54d288 --- /dev/null +++ b/crates/secret-service-proto/src/wire.rs @@ -0,0 +1,9 @@ +use rkyv::{Archive, Deserialize, Serialize}; + +use crate::v1; + +#[repr(u8)] +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum VersionedClientMessage { + V1(v1::wire::ClientMessage), +} diff --git a/crates/secret-service-server/Cargo.toml b/crates/secret-service-server/Cargo.toml new file mode 100644 index 00000000..ecd3599c --- /dev/null +++ b/crates/secret-service-server/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "secret-service-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitcoin.workspace = true +futures.workspace = true +kanal.workspace = true +musig2.workspace = true +parking_lot.workspace = true +quinn.workspace = true +rkyv.workspace = true +secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } +terrors.workspace = true +tokio.workspace = true +tracing.workspace = true diff --git a/crates/secret-service-server/src/bool_arr.rs b/crates/secret-service-server/src/bool_arr.rs new file mode 100644 index 00000000..d1ee7f78 --- /dev/null +++ b/crates/secret-service-server/src/bool_arr.rs @@ -0,0 +1,69 @@ +use std::marker::PhantomData; + +// Kind of like a bitmap for storing bools as bits, but instead we're storing +// (bool, bool) instead of just a bool, allowing us to check up to 4 different +// states. Each 64 bit word can store 32 of these slots. +pub struct DoubleBoolArray([u64; N], PhantomData) +where + T: Into<(bool, bool)> + TryFrom<(bool, bool)>, + >::Error: std::fmt::Debug; + +impl Default for DoubleBoolArray +where + T: Into<(bool, bool)> + TryFrom<(bool, bool)>, + >::Error: std::fmt::Debug, +{ + fn default() -> Self { + Self([0; N], PhantomData) + } +} + +impl DoubleBoolArray +where + T: Into<(bool, bool)> + TryFrom<(bool, bool)>, + >::Error: std::fmt::Debug, +{ + pub const fn capacity() -> usize { + N * (std::mem::size_of::() * 8 / 2) + } + + pub fn find_next_empty_slot(&self) -> Option { + for (chunk_idx, &chunk) in self.0.iter().enumerate() { + for slot in 0..32 { + let mask = 0b11 << (slot * 2); + if (chunk & mask) == 0 { + return Some(chunk_idx * 32 + slot); + } + } + } + None + } + + /// Get the two boolean values at specified index + /// Panics if index >= N * 32 + pub fn get(&self, index: usize) -> T { + assert!(index < N * 32, "Index out of bounds"); + let chunk_idx = index / 32; + let slot = index % 32; + let chunk = self.0[chunk_idx]; + + let bits = (chunk >> (slot * 2)) & 0b11; + T::try_from(((bits & 0b01) != 0, (bits & 0b10) != 0)) + .expect("T::try_from(T::Into) should always succeed") + } + + /// Set the two boolean values at specified index + /// Panics if index >= N * 32 + pub fn set(&mut self, index: usize, value: T) { + assert!(index < N * 32, "Index out of bounds"); + let chunk_idx = index / 32; + let slot = index % 32; + let chunk = &mut self.0[chunk_idx]; + + let mask = !(0b11 << (slot * 2)); + let values: (bool, bool) = value.into(); + let new_bits = (values.0 as u64) | ((values.1 as u64) << 1); + + *chunk = (*chunk & mask) | (new_bits << (slot * 2)); + } +} diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs new file mode 100644 index 00000000..f6acb2b2 --- /dev/null +++ b/crates/secret-service-server/src/lib.rs @@ -0,0 +1,492 @@ +pub mod bool_arr; +pub mod ms2sm; + +use std::{ + future::Future, + io, + marker::{PhantomData, Sync}, + mem::MaybeUninit, + net::SocketAddr, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use bitcoin::{secp256k1::PublicKey, Psbt}; +use kanal::AsyncSender; +use ms2sm::Musig2SessionManager; +use musig2::{errors::RoundFinalizeError, LiftedSignature, PartialSignature, PubNonce}; +pub use quinn::rustls; +use quinn::{ + crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, + ConnectionError, Endpoint, Incoming, ReadExactError, RecvStream, SendStream, ServerConfig, + WriteError, +}; +use rkyv::rancor::{self, Error}; +use secret_service_proto::{ + v1::{ + traits::{ + Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, + P2PSigner, SecretService, Server, WotsSigner, + }, + wire::{ArchivedClientMessage, ServerMessage}, + }, + wire::ArchivedVersionedClientMessage, +}; +use terrors::OneOf; +use tokio::{ + sync::Mutex, + task::{JoinError, JoinHandle}, +}; +use tracing::{error, span, warn, Instrument, Level}; + +pub struct Config { + addr: SocketAddr, + connection_limit: Option, + tls_config: rustls::ServerConfig, +} + +pub struct ServerHandle { + main: JoinHandle<()>, +} + +impl Future for ServerHandle { + type Output = Result<(), JoinError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.get_mut().main).poll(cx) + } +} + +pub fn run_server( + c: Config, + service: Arc, +) -> Result> +where + SecondRound: Musig2SignerSecondRound + 'static, + FirstRound: Musig2SignerFirstRound + 'static, + Service: SecretService + Sync + 'static, +{ + let quic_server_config = ServerConfig::with_crypto(Arc::new( + QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, + )); + let endpoint = Endpoint::server(quic_server_config, c.addr).map_err(OneOf::new)?; + let main_task = async move { + let musig2_sm = Arc::new(Mutex::new( + Musig2SessionManager::::default(), + )); + while let Some(incoming) = endpoint.accept().await { + let span = span!(Level::INFO, + "connection", + cid = %incoming.orig_dst_cid(), + remote = %incoming.remote_address(), + remote_validated = %incoming.remote_address_validated() + ); + if matches!(c.connection_limit, Some(n) if endpoint.open_connections() >= n) { + incoming.refuse(); + } else { + tokio::spawn( + conn_handler(incoming, service.clone(), musig2_sm.clone()).instrument(span), + ); + } + } + }; + let handle = tokio::spawn(main_task); + Ok(ServerHandle { main: handle }) +} + +async fn conn_handler( + incoming: Incoming, + service: Arc, + musig2_sm: Arc>>, +) where + SecondRound: Musig2SignerSecondRound + 'static, + FirstRound: Musig2SignerFirstRound + 'static, + Service: SecretService + Sync + 'static, +{ + let conn = match incoming.await { + Ok(conn) => conn, + Err(e) => { + warn!("accepting incoming conn failed: {e:?}"); + return; + } + }; + + let mut req_id: usize = 0; + let (err_tx, err_rx) = kanal::unbounded_async(); + + tokio::spawn( + async move { + while let Ok(io_err) = err_rx.recv().await { + match io_err { + IoError::WriteError(e) => { + warn!("write error: {e:?}"); + } + IoError::ReadError(e) => { + warn!("read error: {e:?}"); + } + } + } + } + .instrument(span!(Level::INFO, "conn-error-handler", cid = %conn.stable_id())), + ); + + loop { + let (tx, rx) = match conn.accept_bi().await { + Ok(txers) => txers, + Err(ConnectionError::ApplicationClosed(_)) => return, + Err(e) => { + warn!("accepting incoming stream failed: {e:?}"); + continue; + } + }; + req_id = req_id.wrapping_add(1); + let span = span!(Level::INFO, "stream", cid = %conn.stable_id(), rid = req_id); + tokio::spawn( + request_handler(tx, rx, service.clone(), musig2_sm.clone(), err_tx.clone()) + .instrument(span), + ); + } +} + +async fn request_handler( + mut tx: SendStream, + mut rx: RecvStream, + service: Arc, + musig2_sm: Arc>>, + err_tx: AsyncSender, +) where + SecondRound: Musig2SignerSecondRound, + FirstRound: Musig2SignerFirstRound, + Service: SecretService, +{ + let len_to_read = { + let mut buf = 0u16.to_le_bytes(); + if let Err(e) = rx.read_exact(&mut buf).await { + let _ = err_tx.send(e.into()).await; + return; + } + u16::from_le_bytes(buf) + }; + + let mut buf = vec![0u8; len_to_read as usize]; + if let Err(e) = rx.read_exact(&mut buf).await { + let _ = err_tx.send(e.into()).await; + return; + } + + let msg = rkyv::access::(&buf).unwrap(); + let res = match msg { + // this would be a separate function but tokio would start whining because !Sync + ArchivedVersionedClientMessage::V1(req) => match req { + ArchivedClientMessage::OperatorSignPsbt { psbt } => { + let psbt = Psbt::deserialize(&psbt).unwrap(); + let r = service.operator_signer().sign_psbt(psbt).await; + ServerMessage::::OperatorSignPsbt( + r.map(|psbt| psbt.serialize()), + ) + } + + ArchivedClientMessage::SignP2P { hash } => { + let r = service.p2p_signer().sign_p2p(*hash).await; + ServerMessage::::SignP2P(r) + } + + ArchivedClientMessage::Musig2NewSession => { + let first_round = service.musig2_signer().new_session().await; + match musig2_sm.lock().await.new_session(first_round) { + Some(id) => { + ServerMessage::::Musig2NewSession(id) + } + None => ServerMessage::::OpaqueServerError, + } + } + ArchivedClientMessage::Musig2FirstRoundOurNonce { session_id } => { + let r = musig2_sm + .lock() + .await + .first_round(session_id.to_native() as usize); + match r { + Ok(Some(first_round)) => { + let nonce = first_round.our_nonce().await.serialize(); + ServerMessage::::Musig2FirstRoundOurNonce( + nonce, + ) + } + _ => ServerMessage::::InvalidClientMessage, + } + } + ArchivedClientMessage::Musig2FirstRoundHoldouts { session_id } => { + let r = musig2_sm + .lock() + .await + .first_round(session_id.to_native() as usize); + match r { + Ok(Some(first_round)) => { + ServerMessage::::Musig2FirstRoundHoldouts( + first_round + .holdouts() + .await + .iter() + .map(PublicKey::serialize) + .collect(), + ) + } + _ => ServerMessage::::InvalidClientMessage, + } + } + ArchivedClientMessage::Musig2FirstRoundIsComplete { session_id } => { + let r = musig2_sm + .lock() + .await + .first_round(session_id.to_native() as usize); + match r { + Ok(Some(first_round)) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2FirstRoundIsComplete( + first_round.is_complete().await + ), + _ => { + ServerMessage::< + Service, + SecondRound, + FirstRound, + >::InvalidClientMessage + } + } + } + ArchivedClientMessage::Musig2FirstRoundReceivePubNonce { + session_id, + pubkey, + pubnonce, + } => { + let r = musig2_sm + .lock() + .await + .first_round(session_id.to_native() as usize); + let pubkey = PublicKey::from_slice(pubkey); + let pubnonce = PubNonce::from_bytes(pubnonce); + match (r, pubkey, pubnonce) { + (Ok(Some(first_round)), Ok(pubkey), Ok(pubnonce)) => { + let r = first_round.receive_pub_nonce(pubkey, pubnonce).await; + ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2FirstRoundReceivePubNonce(r.err()) + } + _ => ServerMessage::::InvalidClientMessage, + } + } + ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => { + let r = musig2_sm + .lock() + .await + .transition_first_to_second_round(session_id.to_native() as usize, *hash) + .await; + + if let Err(e) = r { + use terrors::E3::*; + match e.narrow::() { + Ok(e) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2FirstRoundFinalize(Some(e)), + Err(e) => match e.as_enum() { + A(_not_in_first_round) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::InvalidClientMessage, + B(_out_of_range) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::InvalidClientMessage, + C(_other_refs_active) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::OpaqueServerError, + }, + } + } else { + ServerMessage::::Musig2FirstRoundFinalize( + None, + ) + } + } + + ArchivedClientMessage::Musig2SecondRoundAggNonce { session_id } => { + let sr = musig2_sm + .lock() + .await + .second_round(session_id.to_native() as usize); + + match sr { + Ok(Some(sr)) => { + ServerMessage::::Musig2SecondRoundAggNonce( + sr.agg_nonce().await.serialize(), + ) + } + _ => ServerMessage::::InvalidClientMessage, + } + } + ArchivedClientMessage::Musig2SecondRoundHoldouts { session_id } => { + let sr = musig2_sm + .lock() + .await + .second_round(session_id.to_native() as usize); + + match sr { + Ok(Some(sr)) => { + ServerMessage::::Musig2SecondRoundHoldouts( + sr.holdouts() + .await + .iter() + .map(PublicKey::serialize) + .collect(), + ) + } + _ => ServerMessage::::InvalidClientMessage, + } + } + ArchivedClientMessage::Musig2SecondRoundOurSignature { session_id } => { + let sr = musig2_sm + .lock() + .await + .second_round(session_id.to_native() as usize); + + match sr { + Ok(Some(sr)) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2SecondRoundOurSignature( + sr.our_signature().await.serialize() + ), + _ => { + ServerMessage::< + Service, + SecondRound, + FirstRound, + >::InvalidClientMessage + } + } + } + ArchivedClientMessage::Musig2SecondRoundIsComplete { session_id } => { + let sr = musig2_sm + .lock() + .await + .second_round(session_id.to_native() as usize); + + match sr { + Ok(Some(sr)) => ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2SecondRoundIsComplete( + sr.is_complete().await + ), + _ => { + ServerMessage::< + Service, + SecondRound, + FirstRound, + >::InvalidClientMessage + } + } + } + ArchivedClientMessage::Musig2SecondRoundReceiveSignature { + session_id, + pubkey, + signature, + } => { + let sr = musig2_sm + .lock() + .await + .second_round(session_id.to_native() as usize); + let pubkey = PublicKey::from_slice(pubkey); + let signature = PartialSignature::from_slice(signature); + match (sr, pubkey, signature) { + (Ok(Some(sr)), Ok(pubkey), Ok(signature)) => { + let r = sr.receive_signature(pubkey, signature).await; + ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2SecondRoundReceiveSignature(r.err()) + } + _ => ServerMessage::::InvalidClientMessage, + } + } + ArchivedClientMessage::Musig2SecondRoundFinalize { session_id } => { + let r = musig2_sm + .lock() + .await + .finalize_second_round(session_id.to_native() as usize) + .await; + match r { + Ok(sig) => { + ServerMessage::::Musig2SecondRoundFinalize( + Ok(sig.serialize()).into(), + ) + } + Err(e) => { + if let Ok(e) = e.narrow::() { + ServerMessage::< + Service, + SecondRound, + FirstRound, + >::Musig2SecondRoundFinalize(Err(e).into()) + } else { + ServerMessage::::InvalidClientMessage + } + } + } + } + + ArchivedClientMessage::WotsGetKey { index } => { + let r = service.wots_signer().get_key(index.into()).await; + ServerMessage::::WotsGetKey(r) + } + }, + }; + + let byte_response = match rkyv::to_bytes::(&res) { + Ok(r) => r, + Err(e) => { + error!("failed to serialize response: {e:?}"); + let res = ServerMessage::::OpaqueServerError; + if let Ok(bytes) = rkyv::to_bytes::(&res) { + if let Err(e) = tx.write(&bytes).await { + let _ = err_tx.send(e.into()).await; + } + } + return; + } + }; + if let Err(e) = tx.write(&byte_response).await { + let _ = err_tx.send(IoError::WriteError(e)).await; + } +} + +enum IoError { + WriteError(WriteError), + ReadError(ReadExactError), +} + +impl From for IoError { + fn from(e: WriteError) -> Self { + IoError::WriteError(e) + } +} + +impl From for IoError { + fn from(e: ReadExactError) -> Self { + IoError::ReadError(e) + } +} diff --git a/crates/secret-service-server/src/ms2sm.rs b/crates/secret-service-server/src/ms2sm.rs new file mode 100644 index 00000000..b47f136d --- /dev/null +++ b/crates/secret-service-server/src/ms2sm.rs @@ -0,0 +1,207 @@ +use std::{mem::MaybeUninit, sync::Arc}; + +use musig2::{errors::RoundFinalizeError, LiftedSignature}; +use secret_service_proto::v1::traits::{Musig2SignerFirstRound, Musig2SignerSecondRound, Server}; +use terrors::OneOf; + +use crate::bool_arr::DoubleBoolArray; + +pub struct Musig2SessionManager +where + SecondRound: Musig2SignerSecondRound, + FirstRound: Musig2SignerFirstRound, +{ + /// Tracker is used for tracking whether a session is in first round, + /// second round or completed. N=128 means we can track 128*32=4096 sessions + tracker: DoubleBoolArray, + /// Used to store first rounds of musig2 server instances. This is a Vec + /// because we don't know how big FirstRound may be in memory so we will + /// heap allocate and try keep this to a minimum + first_rounds: Vec>>, + /// Used to store second rounds of musig2 server instances. This is a Vec + /// because we don't know how big SecondRound may be in memory so we will + /// heap allocate and try keep this to a minimum + second_rounds: Vec>>, +} + +impl Default + for Musig2SessionManager +where + SecondRound: Musig2SignerSecondRound, + FirstRound: Musig2SignerFirstRound, +{ + fn default() -> Self { + Self { + tracker: DoubleBoolArray::default(), + first_rounds: Vec::new(), + second_rounds: Vec::new(), + } + } +} + +#[derive(Debug)] +pub struct OutOfRange; + +#[derive(Debug)] +pub struct NotInCorrectRound { + wanted: SlotState, + got: SlotState, +} + +#[derive(Debug)] +pub struct OtherReferencesActive; + +impl Musig2SessionManager +where + SecondRound: Musig2SignerSecondRound, + FirstRound: Musig2SignerFirstRound, +{ + pub fn new_session(&mut self, first_round: FirstRound) -> Option { + let next_empty = self.tracker.find_next_empty_slot()?; + if next_empty <= self.first_rounds.len() { + // we're replacing an existing session + self.first_rounds[next_empty] = MaybeUninit::new(first_round.into()); + } else { + // we're not replacing any existing session, so we need to grow + self.first_rounds.push(MaybeUninit::new(first_round.into())); + } + return Some(next_empty); + } + + #[inline] + fn slot_state(&self, session_id: usize) -> Result { + match session_id < DoubleBoolArray::::capacity() { + true => Ok(self.tracker.get(session_id)), + false => Err(OutOfRange), + } + } + + pub async fn transition_first_to_second_round( + &mut self, + session_id: usize, + hash: [u8; 32], + ) -> Result< + (), + OneOf<( + NotInCorrectRound, + OutOfRange, + OtherReferencesActive, + RoundFinalizeError, + )>, + > { + match self.slot_state(session_id).map_err(OneOf::new)? { + SlotState::FirstRound => { + let arc = unsafe { + std::mem::replace( + self.first_rounds.get_unchecked_mut(session_id), + MaybeUninit::uninit(), + ) + .assume_init() + }; + let first_round = match Arc::try_unwrap(arc) { + Ok(fr) => fr, + Err(arc) => { + self.first_rounds[session_id] = MaybeUninit::new(arc); + return Err(OneOf::new(OtherReferencesActive)); + } + }; + let second_round = first_round.finalize(hash).await.map_err(OneOf::new)?; + self.second_rounds[session_id] = MaybeUninit::new(second_round.into()); + self.tracker.set(session_id, SlotState::SecondRound); + Ok(()) + } + slot_state => Err(OneOf::new(NotInCorrectRound { + wanted: SlotState::FirstRound, + got: slot_state, + })), + } + } + + pub async fn finalize_second_round( + &mut self, + session_id: usize, + ) -> Result< + LiftedSignature, + OneOf<( + OutOfRange, + NotInCorrectRound, + OtherReferencesActive, + RoundFinalizeError, + )>, + > { + match self.slot_state(session_id).map_err(OneOf::new)? { + SlotState::SecondRound => { + let arc = unsafe { + std::mem::replace( + self.second_rounds.get_unchecked_mut(session_id), + MaybeUninit::uninit(), + ) + .assume_init() + }; + let second_round = match Arc::try_unwrap(arc) { + Ok(sr) => sr, + Err(arc) => { + self.second_rounds[session_id] = MaybeUninit::new(arc); + return Err(OneOf::new(OtherReferencesActive)); + } + }; + self.tracker.set(session_id, SlotState::Empty); + Ok(second_round.finalize().await.map_err(OneOf::new)?) + } + slot_state => Err(OneOf::new(NotInCorrectRound { + wanted: SlotState::SecondRound, + got: slot_state, + })), + } + } + + pub fn first_round(&self, session_id: usize) -> Result>, OutOfRange> { + match self.slot_state(session_id)? { + SlotState::FirstRound => { + let first_round = unsafe { self.first_rounds[session_id].assume_init_ref() }; + Ok(Some(first_round.clone())) + } + _ => Ok(None), + } + } + + pub fn second_round(&self, session_id: usize) -> Result>, OutOfRange> { + match self.slot_state(session_id)? { + SlotState::SecondRound => { + let second_round = unsafe { self.second_rounds[session_id].assume_init_ref() }; + Ok(Some(second_round.clone())) + } + _ => Ok(None), + } + } +} + +#[derive(Debug)] +enum SlotState { + Empty, + FirstRound, + SecondRound, +} + +impl TryFrom<(bool, bool)> for SlotState { + type Error = (); + + fn try_from((a, b): (bool, bool)) -> Result { + match (a, b) { + (false, false) => Ok(Self::Empty), + (true, false) => Ok(Self::FirstRound), + (false, true) => Ok(Self::SecondRound), + _ => Err(()), + } + } +} + +impl From for (bool, bool) { + fn from(state: SlotState) -> Self { + match state { + SlotState::Empty => (false, false), + SlotState::FirstRound => (true, false), + SlotState::SecondRound => (false, true), + } + } +} diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml new file mode 100644 index 00000000..c7ed7d1e --- /dev/null +++ b/crates/secret-service/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "secret-service" +version = "0.1.0" +edition = "2021" + +[dependencies] +bitcoin.workspace = true +blake3.workspace = true +musig2.workspace = true +secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } +secret-service-server = { version = "0.1.0", path = "../secret-service-server" } +strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } + +tokio.workspace = true diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs new file mode 100644 index 00000000..05f87c82 --- /dev/null +++ b/crates/secret-service/src/main.rs @@ -0,0 +1,8 @@ +use secret_service_server::rustls::ServerConfig; + +#[tokio::main] +async fn main() { + // let config = ServerConfig::builder() + // .with_client_cert_verifier(client_cert_verifier) + println!("Hello, world!"); +} From a11a2e39966b54883edeb82effc3862f28805948 Mon Sep 17 00:00:00 2001 From: Azz Date: Fri, 24 Jan 2025 15:40:17 +0000 Subject: [PATCH 03/30] lock update --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6244526a..59e3d009 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,7 +80,7 @@ bitcoin = { version = "0.32.5", features = ["rand-std", "serde"] } bitcoin-bosd = "0.4.0" bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" } bitvm = { git = "https://github.com/alpenlabs/BitVM.git", branch = "testnet-i" } -blake3 = { version = "1.5.5", features = ["zeroize"] } +blake3 = { version = "1.5", features = ["zeroize"] } borsh = { version = "1.5.0", features = ["derive"] } chrono = "0.4.38" clap = { version = "4.5.20", features = ["cargo", "derive", "env"] } From 1ed2d007287f26c75b3fb02be21da7834f377fdf Mon Sep 17 00:00:00 2001 From: Azz Date: Fri, 24 Jan 2025 16:53:12 +0000 Subject: [PATCH 04/30] happy friday :P --- crates/secret-service-client/Cargo.toml | 1 + crates/secret-service-client/src/lib.rs | 99 +++++-- crates/secret-service-proto/src/v1/traits.rs | 14 +- crates/secret-service-proto/src/v1/wire.rs | 27 +- crates/secret-service-server/src/lib.rs | 295 +++++++------------ crates/secret-service/src/main.rs | 2 +- 6 files changed, 215 insertions(+), 223 deletions(-) diff --git a/crates/secret-service-client/Cargo.toml b/crates/secret-service-client/Cargo.toml index 3bf9d25a..e963707a 100644 --- a/crates/secret-service-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +bitcoin.workspace = true kanal.workspace = true musig2.workspace = true quinn.workspace = true diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 010e8a8b..3a9cd1e7 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -5,6 +5,7 @@ use std::{ sync::Arc, }; +use bitcoin::Psbt; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::PublicKey, @@ -15,8 +16,8 @@ use quinn::{ rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, }; use secret_service_proto::v1::traits::{ - self, Client, Musig2SessionId, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, - SecretService, + Client, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, + OperatorSigner, Origin, P2PSigner, SecretService, WotsSigner, }; use terrors::OneOf; @@ -76,6 +77,7 @@ impl SecretServiceClient { struct Musig2FirstRound { session_id: Musig2SessionId, + connection: Connection, } impl Musig2SignerFirstRound for Musig2FirstRound { @@ -114,6 +116,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { struct Musig2SecondRound { session_id: Musig2SessionId, + connection: Connection, } impl Musig2SignerSecondRound for Musig2SecondRound { @@ -157,28 +160,84 @@ impl Musig2SignerSecondRound for Musig2SecondRound { } } -// impl SecretService for SecretServiceClient { -// type OperatorSigner; +impl SecretService for SecretServiceClient { + type OperatorSigner = OperatorClient; -// type P2PSigner; + type P2PSigner = P2PClient; -// type Musig2Signer; + type Musig2Signer = Musig2Client; -// type WotsSigner; + type WotsSigner = WotsClient; -// fn operator_signer(&self) -> &Self::OperatorSigner { -// todo!() -// } + fn operator_signer(&self) -> Self::OperatorSigner { + OperatorClient(self.conn.as_ref().unwrap().clone()) + } + + fn p2p_signer(&self) -> Self::P2PSigner { + todo!() + } + + fn musig2_signer(&self) -> Self::Musig2Signer { + todo!() + } + + fn wots_signer(&self) -> Self::WotsSigner { + todo!() + } +} + +struct OperatorClient(Connection); -// fn p2p_signer(&self) -> &Self::P2PSigner { -// todo!() -// } +impl OperatorSigner for OperatorClient { + type OperatorSigningError = (); -// fn musig2_signer(&self) -> &Self::Musig2Signer { -// todo!() -// } + fn sign_psbt( + &self, + psbt: Psbt, + ) -> impl Future::Container>> + + Send { + async move { todo!() } + } +} + +struct P2PClient(Connection); -// fn wots_signer(&self) -> &Self::WotsSigner { -// todo!() -// } -// } +impl P2PSigner for P2PClient { + type P2PSigningError = (); + + fn sign_p2p( + &self, + hash: [u8; 32], + ) -> impl Future::Container>> + + Send { + async move { todo!() } + } + + fn p2p_pubkey(&self) -> impl Future + Send { + async move { todo!() } + } +} + +struct Musig2Client(Connection); + +impl Musig2Signer for Musig2Client { + fn new_session( + &self, + ) -> impl Future::Container> + Send { + async move { + // self.0.open_bi(); + todo!() + } + } +} + +struct WotsClient(Connection); + +impl WotsSigner for WotsClient { + fn get_key( + &self, + index: u64, + ) -> impl Future::Container<[u8; 64]>> + Send { + async move { todo!() } + } +} diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 39a5c35d..786ab66a 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -10,20 +10,20 @@ use rkyv::{ api::high::HighSerializer, rancor, ser::allocator::ArenaHandle, util::AlignedVec, Serialize, }; -pub trait SecretServiceFactory: Send + Clone +pub trait SecretServiceFactory: Send + Clone where FirstRound: Musig2SignerFirstRound, SecondRound: Musig2SignerSecondRound, { type Context: Send + Clone; - type Service: SecretService + Send; + type Service: SecretService + Send; fn produce(ctx: Self::Context) -> Self::Service; } // possible when https://github.com/rust-lang/rust/issues/63063 is stabliized // pub type AsyncResult = impl Future>; -pub trait SecretService: Send +pub trait SecretService: Send where O: Origin, FirstRound: Musig2SignerFirstRound, @@ -33,10 +33,10 @@ where type Musig2Signer: Musig2Signer; type WotsSigner: WotsSigner; - fn operator_signer(&self) -> &Self::OperatorSigner; - fn p2p_signer(&self) -> &Self::P2PSigner; - fn musig2_signer(&self) -> &Self::Musig2Signer; - fn wots_signer(&self) -> &Self::WotsSigner; + fn operator_signer(&self) -> Self::OperatorSigner; + fn p2p_signer(&self) -> Self::P2PSigner; + fn musig2_signer(&self) -> Self::Musig2Signer; + fn wots_signer(&self) -> Self::WotsSigner; } pub trait OperatorSigner: Send { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 0dbf923a..983a7f01 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -18,9 +18,9 @@ trait WireMessageMarker: } #[derive(Debug, Clone, Archive, Serialize, Deserialize)] -pub enum ServerMessage +pub enum ServerMessage where - S: SecretService, + S: SecretService, FirstRound: Musig2SignerFirstRound, { InvalidClientMessage, @@ -58,6 +58,13 @@ where WotsGetKey([u8; 64]), } +impl WireMessageMarker for ServerMessage +where + S: SecretService, + FirstRound: Musig2SignerFirstRound, +{ +} + #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum Musig2SessionResult { Ok([u8; 64]), @@ -151,15 +158,17 @@ pub trait WireMessage { impl WireMessage for T { fn serialize(&self) -> Result { let mut aligned_buf = AlignedVec::new(); - // write a default length aligned_buf.extend_from_slice(&u32::MAX.to_le_bytes()); let mut aligned_buf = to_bytes_in(self, aligned_buf)?; - let len = aligned_buf.len() - 4; - debug_assert!(len <= u32::MAX as usize); - let len_as_le_bytes = (len as u32).to_le_bytes(); - for i in 0..4 { - aligned_buf[i] = len_as_le_bytes[i] - } + let len = aligned_buf.len() - size_of::(); + assert!(len <= u32::MAX as usize); + (len as u32) + .to_le_bytes() + .into_iter() + .enumerate() + .for_each(|byte| { + aligned_buf[byte.0] = byte.1; + }); Ok(aligned_buf) } } diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index f6acb2b2..d75b5dd3 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -4,8 +4,7 @@ pub mod ms2sm; use std::{ future::Future, io, - marker::{PhantomData, Sync}, - mem::MaybeUninit, + marker::Sync, net::SocketAddr, pin::Pin, sync::Arc, @@ -15,7 +14,7 @@ use std::{ use bitcoin::{secp256k1::PublicKey, Psbt}; use kanal::AsyncSender; use ms2sm::Musig2SessionManager; -use musig2::{errors::RoundFinalizeError, LiftedSignature, PartialSignature, PubNonce}; +use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; pub use quinn::rustls; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, @@ -29,7 +28,7 @@ use secret_service_proto::{ Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, P2PSigner, SecretService, Server, WotsSigner, }, - wire::{ArchivedClientMessage, ServerMessage}, + wire::{ArchivedClientMessage, ServerMessage, WireMessage}, }, wire::ArchivedVersionedClientMessage, }; @@ -58,14 +57,14 @@ impl Future for ServerHandle { } } -pub fn run_server( +pub fn run_server( c: Config, service: Arc, ) -> Result> where - SecondRound: Musig2SignerSecondRound + 'static, FirstRound: Musig2SignerFirstRound + 'static, - Service: SecretService + Sync + 'static, + SecondRound: Musig2SignerSecondRound + 'static, + Service: SecretService + Sync + 'static, { let quic_server_config = ServerConfig::with_crypto(Arc::new( QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, @@ -95,14 +94,14 @@ where Ok(ServerHandle { main: handle }) } -async fn conn_handler( +async fn conn_handler( incoming: Incoming, service: Arc, musig2_sm: Arc>>, ) where - SecondRound: Musig2SignerSecondRound + 'static, FirstRound: Musig2SignerFirstRound + 'static, - Service: SecretService + Sync + 'static, + SecondRound: Musig2SignerSecondRound + 'static, + Service: SecretService + Sync + 'static, { let conn = match incoming.await { Ok(conn) => conn, @@ -113,24 +112,6 @@ async fn conn_handler( }; let mut req_id: usize = 0; - let (err_tx, err_rx) = kanal::unbounded_async(); - - tokio::spawn( - async move { - while let Ok(io_err) = err_rx.recv().await { - match io_err { - IoError::WriteError(e) => { - warn!("write error: {e:?}"); - } - IoError::ReadError(e) => { - warn!("read error: {e:?}"); - } - } - } - } - .instrument(span!(Level::INFO, "conn-error-handler", cid = %conn.stable_id())), - ); - loop { let (tx, rx) = match conn.accept_bi().await { Ok(txers) => txers, @@ -141,64 +122,95 @@ async fn conn_handler( } }; req_id = req_id.wrapping_add(1); - let span = span!(Level::INFO, "stream", cid = %conn.stable_id(), rid = req_id); + let handler_span = + span!(Level::INFO, "request handler", cid = %conn.stable_id(), rid = req_id); + let manager_span = + span!(Level::INFO, "request manager", cid = %conn.stable_id(), rid = req_id); tokio::spawn( - request_handler(tx, rx, service.clone(), musig2_sm.clone(), err_tx.clone()) - .instrument(span), + request_manager( + tx, + tokio::spawn( + request_handler(rx, service.clone(), musig2_sm.clone()) + .instrument(handler_span), + ), + ) + .instrument(manager_span), ); } } -async fn request_handler( +async fn request_manager( mut tx: SendStream, + handler: JoinHandle, ReadExactError>>, +) where + FirstRound: Musig2SignerFirstRound, + SecondRound: Musig2SignerSecondRound, + Service: SecretService, +{ + let handler_res = match handler.await { + Ok(r) => r, + Err(e) => { + error!("request handler failed: {e:?}"); + return; + } + }; + + match handler_res { + Ok(msg) => { + let byte_response = match WireMessage::serialize(&msg) { + Ok(r) => r, + Err(e) => { + error!("failed to serialize response: {e:?}"); + return; + } + }; + if let Err(e) = tx.write_all(&byte_response).await { + warn!("failed to send response: {e:?}"); + } + } + Err(e) => warn!("handler failed to read: {e:?}"), + } +} + +async fn request_handler( mut rx: RecvStream, service: Arc, musig2_sm: Arc>>, - err_tx: AsyncSender, -) where - SecondRound: Musig2SignerSecondRound, +) -> Result, ReadExactError> +where FirstRound: Musig2SignerFirstRound, - Service: SecretService, + SecondRound: Musig2SignerSecondRound, + Service: SecretService, { let len_to_read = { let mut buf = 0u16.to_le_bytes(); - if let Err(e) = rx.read_exact(&mut buf).await { - let _ = err_tx.send(e.into()).await; - return; - } + rx.read_exact(&mut buf).await?; u16::from_le_bytes(buf) }; let mut buf = vec![0u8; len_to_read as usize]; - if let Err(e) = rx.read_exact(&mut buf).await { - let _ = err_tx.send(e.into()).await; - return; - } + rx.read_exact(&mut buf).await?; let msg = rkyv::access::(&buf).unwrap(); - let res = match msg { + Ok(match msg { // this would be a separate function but tokio would start whining because !Sync ArchivedVersionedClientMessage::V1(req) => match req { ArchivedClientMessage::OperatorSignPsbt { psbt } => { let psbt = Psbt::deserialize(&psbt).unwrap(); let r = service.operator_signer().sign_psbt(psbt).await; - ServerMessage::::OperatorSignPsbt( - r.map(|psbt| psbt.serialize()), - ) + ServerMessage::OperatorSignPsbt(r.map(|psbt| psbt.serialize())) } ArchivedClientMessage::SignP2P { hash } => { let r = service.p2p_signer().sign_p2p(*hash).await; - ServerMessage::::SignP2P(r) + ServerMessage::SignP2P(r) } ArchivedClientMessage::Musig2NewSession => { let first_round = service.musig2_signer().new_session().await; match musig2_sm.lock().await.new_session(first_round) { - Some(id) => { - ServerMessage::::Musig2NewSession(id) - } - None => ServerMessage::::OpaqueServerError, + Some(id) => ServerMessage::Musig2NewSession(id), + None => ServerMessage::OpaqueServerError, } } ArchivedClientMessage::Musig2FirstRoundOurNonce { session_id } => { @@ -209,11 +221,9 @@ async fn request_handler( match r { Ok(Some(first_round)) => { let nonce = first_round.our_nonce().await.serialize(); - ServerMessage::::Musig2FirstRoundOurNonce( - nonce, - ) + ServerMessage::Musig2FirstRoundOurNonce(nonce) } - _ => ServerMessage::::InvalidClientMessage, + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2FirstRoundHoldouts { session_id } => { @@ -222,17 +232,15 @@ async fn request_handler( .await .first_round(session_id.to_native() as usize); match r { - Ok(Some(first_round)) => { - ServerMessage::::Musig2FirstRoundHoldouts( - first_round - .holdouts() - .await - .iter() - .map(PublicKey::serialize) - .collect(), - ) - } - _ => ServerMessage::::InvalidClientMessage, + Ok(Some(first_round)) => ServerMessage::Musig2FirstRoundHoldouts( + first_round + .holdouts() + .await + .iter() + .map(PublicKey::serialize) + .collect(), + ), + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2FirstRoundIsComplete { session_id } => { @@ -241,20 +249,10 @@ async fn request_handler( .await .first_round(session_id.to_native() as usize); match r { - Ok(Some(first_round)) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2FirstRoundIsComplete( - first_round.is_complete().await - ), - _ => { - ServerMessage::< - Service, - SecondRound, - FirstRound, - >::InvalidClientMessage + Ok(Some(first_round)) => { + ServerMessage::Musig2FirstRoundIsComplete(first_round.is_complete().await) } + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2FirstRoundReceivePubNonce { @@ -271,13 +269,9 @@ async fn request_handler( match (r, pubkey, pubnonce) { (Ok(Some(first_round)), Ok(pubkey), Ok(pubnonce)) => { let r = first_round.receive_pub_nonce(pubkey, pubnonce).await; - ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2FirstRoundReceivePubNonce(r.err()) + ServerMessage::Musig2FirstRoundReceivePubNonce(r.err()) } - _ => ServerMessage::::InvalidClientMessage, + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => { @@ -290,33 +284,15 @@ async fn request_handler( if let Err(e) = r { use terrors::E3::*; match e.narrow::() { - Ok(e) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2FirstRoundFinalize(Some(e)), + Ok(e) => ServerMessage::Musig2FirstRoundFinalize(Some(e)), Err(e) => match e.as_enum() { - A(_not_in_first_round) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::InvalidClientMessage, - B(_out_of_range) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::InvalidClientMessage, - C(_other_refs_active) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::OpaqueServerError, + A(_not_in_first_round) => ServerMessage::InvalidClientMessage, + B(_out_of_range) => ServerMessage::InvalidClientMessage, + C(_other_refs_active) => ServerMessage::OpaqueServerError, }, } } else { - ServerMessage::::Musig2FirstRoundFinalize( - None, - ) + ServerMessage::Musig2FirstRoundFinalize(None) } } @@ -328,11 +304,9 @@ async fn request_handler( match sr { Ok(Some(sr)) => { - ServerMessage::::Musig2SecondRoundAggNonce( - sr.agg_nonce().await.serialize(), - ) + ServerMessage::Musig2SecondRoundAggNonce(sr.agg_nonce().await.serialize()) } - _ => ServerMessage::::InvalidClientMessage, + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2SecondRoundHoldouts { session_id } => { @@ -342,16 +316,14 @@ async fn request_handler( .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => { - ServerMessage::::Musig2SecondRoundHoldouts( - sr.holdouts() - .await - .iter() - .map(PublicKey::serialize) - .collect(), - ) - } - _ => ServerMessage::::InvalidClientMessage, + Ok(Some(sr)) => ServerMessage::Musig2SecondRoundHoldouts( + sr.holdouts() + .await + .iter() + .map(PublicKey::serialize) + .collect(), + ), + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2SecondRoundOurSignature { session_id } => { @@ -361,20 +333,10 @@ async fn request_handler( .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2SecondRoundOurSignature( - sr.our_signature().await.serialize() + Ok(Some(sr)) => ServerMessage::Musig2SecondRoundOurSignature( + sr.our_signature().await.serialize(), ), - _ => { - ServerMessage::< - Service, - SecondRound, - FirstRound, - >::InvalidClientMessage - } + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2SecondRoundIsComplete { session_id } => { @@ -384,20 +346,10 @@ async fn request_handler( .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2SecondRoundIsComplete( - sr.is_complete().await - ), - _ => { - ServerMessage::< - Service, - SecondRound, - FirstRound, - >::InvalidClientMessage + Ok(Some(sr)) => { + ServerMessage::Musig2SecondRoundIsComplete(sr.is_complete().await) } + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2SecondRoundReceiveSignature { @@ -414,13 +366,9 @@ async fn request_handler( match (sr, pubkey, signature) { (Ok(Some(sr)), Ok(pubkey), Ok(signature)) => { let r = sr.receive_signature(pubkey, signature).await; - ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2SecondRoundReceiveSignature(r.err()) + ServerMessage::Musig2SecondRoundReceiveSignature(r.err()) } - _ => ServerMessage::::InvalidClientMessage, + _ => ServerMessage::InvalidClientMessage, } } ArchivedClientMessage::Musig2SecondRoundFinalize { session_id } => { @@ -430,20 +378,12 @@ async fn request_handler( .finalize_second_round(session_id.to_native() as usize) .await; match r { - Ok(sig) => { - ServerMessage::::Musig2SecondRoundFinalize( - Ok(sig.serialize()).into(), - ) - } + Ok(sig) => ServerMessage::Musig2SecondRoundFinalize(Ok(sig.serialize()).into()), Err(e) => { if let Ok(e) = e.narrow::() { - ServerMessage::< - Service, - SecondRound, - FirstRound, - >::Musig2SecondRoundFinalize(Err(e).into()) + ServerMessage::Musig2SecondRoundFinalize(Err(e).into()) } else { - ServerMessage::::InvalidClientMessage + ServerMessage::InvalidClientMessage } } } @@ -451,27 +391,10 @@ async fn request_handler( ArchivedClientMessage::WotsGetKey { index } => { let r = service.wots_signer().get_key(index.into()).await; - ServerMessage::::WotsGetKey(r) + ServerMessage::WotsGetKey(r) } }, - }; - - let byte_response = match rkyv::to_bytes::(&res) { - Ok(r) => r, - Err(e) => { - error!("failed to serialize response: {e:?}"); - let res = ServerMessage::::OpaqueServerError; - if let Ok(bytes) = rkyv::to_bytes::(&res) { - if let Err(e) = tx.write(&bytes).await { - let _ = err_tx.send(e.into()).await; - } - } - return; - } - }; - if let Err(e) = tx.write(&byte_response).await { - let _ = err_tx.send(IoError::WriteError(e)).await; - } + }) } enum IoError { diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 05f87c82..7e562feb 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,4 +1,4 @@ -use secret_service_server::rustls::ServerConfig; +// use secret_service_server::rustls::ServerConfig; #[tokio::main] async fn main() { From dd18ede5047cff94a5470543925437e4353ea9f5 Mon Sep 17 00:00:00 2001 From: Azz Date: Wed, 29 Jan 2025 12:33:07 +0000 Subject: [PATCH 05/30] client --- crates/secret-service-client/src/lib.rs | 407 ++++++++++++++----- crates/secret-service-proto/Cargo.toml | 1 + crates/secret-service-proto/src/v1/traits.rs | 50 +-- crates/secret-service-proto/src/v1/wire.rs | 77 ++-- crates/secret-service-server/src/lib.rs | 101 ++--- 5 files changed, 432 insertions(+), 204 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 3a9cd1e7..3e482a88 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -3,96 +3,172 @@ use std::{ io, net::{Ipv4Addr, SocketAddr}, sync::Arc, + time::Duration, }; use bitcoin::Psbt; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::PublicKey, - PubNonce, + secp256k1::{Error, PublicKey}, + AggNonce, LiftedSignature, PubNonce, }; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, }; -use secret_service_proto::v1::traits::{ - Client, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, - OperatorSigner, Origin, P2PSigner, SecretService, WotsSigner, +use rkyv::{deserialize, rancor}; +use secret_service_proto::v1::{ + traits::{ + Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, + Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, WotsSigner, + }, + wire::{ArchivedServerMessage, ClientMessage, LengthUint, ServerMessage, WireMessage}, }; use terrors::OneOf; +use tokio::time::timeout; #[derive(Clone)] pub struct Config { server_addr: SocketAddr, server_hostname: String, local_addr: Option, - connection_limit: Option, tls_config: rustls::ClientConfig, + timeout: Duration, } #[derive(Clone)] pub struct SecretServiceClient { endpoint: Endpoint, - config: Config, - conn: Option, + config: Arc, + conn: Connection, } impl SecretServiceClient { - pub fn new(config: Config) -> Result { + pub async fn new( + config: Config, + ) -> Result< + Self, + OneOf<( + NoInitialCipherSuite, + ConnectError, + ConnectionError, + io::Error, + )>, + > { let endpoint = Endpoint::client( config .local_addr .unwrap_or((Ipv4Addr::UNSPECIFIED, 0).into()), - )?; - Ok(SecretServiceClient { - endpoint, - config, - conn: None, - }) - } + ) + .map_err(OneOf::new)?; - pub async fn connect( - &mut self, - ) -> Result<(), OneOf<(NoInitialCipherSuite, ConnectError, ConnectionError)>> { - if self.conn.is_some() { - return Ok(()); - } - - let connecting = self - .endpoint + let connecting = endpoint .connect_with( ClientConfig::new(Arc::new( - QuicClientConfig::try_from(self.config.tls_config.clone()) - .map_err(OneOf::new)?, + QuicClientConfig::try_from(config.tls_config.clone()).map_err(OneOf::new)?, )), - self.config.server_addr, - &self.config.server_hostname, + config.server_addr, + &config.server_hostname, ) .map_err(OneOf::new)?; let conn = connecting.await.map_err(OneOf::new)?; - self.conn = Some(conn); - Ok(()) + + Ok(SecretServiceClient { + endpoint, + config: Arc::new(config), + conn, + }) + } +} + +impl SecretService for SecretServiceClient { + type OperatorSigner = OperatorClient; + + type P2PSigner = P2PClient; + + type Musig2Signer = Musig2Client; + + type WotsSigner = WotsClient; + + fn operator_signer(&self) -> Self::OperatorSigner { + OperatorClient { + conn: self.conn.clone(), + config: self.config.clone(), + } + } + + fn p2p_signer(&self) -> Self::P2PSigner { + P2PClient { + conn: self.conn.clone(), + config: self.config.clone(), + } + } + + fn musig2_signer(&self) -> Self::Musig2Signer { + Musig2Client { + conn: self.conn.clone(), + config: self.config.clone(), + } + } + + fn wots_signer(&self) -> Self::WotsSigner { + WotsClient { + conn: self.conn.clone(), + config: self.config.clone(), + } } } struct Musig2FirstRound { session_id: Musig2SessionId, connection: Connection, + config: Arc, } impl Musig2SignerFirstRound for Musig2FirstRound { fn our_nonce(&self) -> impl Future::Container> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2FirstRoundOurNonce { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { + return Err(ClientError::ProtocolError(res)); + }; + PubNonce::from_bytes(&our_nonce).map_err(|_| ClientError::BadData) + } } fn holdouts( &self, ) -> impl Future::Container>> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2FirstRoundHoldouts { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { + return Err(ClientError::ProtocolError(res)); + }; + pubkeys + .into_iter() + .map(|pk| PublicKey::from_slice(&pk)) + .collect::, Error>>() + .map_err(|_| ClientError::BadData) + } } fn is_complete(&self) -> impl Future::Container> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2FirstRoundIsComplete { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(complete) + } } fn receive_pub_nonce( @@ -101,7 +177,18 @@ impl Musig2SignerFirstRound for Musig2FirstRound { pubnonce: PubNonce, ) -> impl Future::Container>> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2FirstRoundReceivePubNonce { + session_id: self.session_id, + pubkey: pubkey.serialize(), + pubnonce: pubnonce.serialize(), + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(maybe_err.map_or(Ok(()), Err)) + } } fn finalize( @@ -110,36 +197,92 @@ impl Musig2SignerFirstRound for Musig2FirstRound { ) -> impl Future< Output = ::Container>, > + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2FirstRoundFinalize { + session_id: self.session_id, + hash, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(match maybe_err { + Some(e) => Err(e), + None => Ok(Musig2SecondRound { + session_id: self.session_id, + connection: self.connection, + config: self.config, + }), + }) + } } } struct Musig2SecondRound { session_id: Musig2SessionId, connection: Connection, + config: Arc, } impl Musig2SignerSecondRound for Musig2SecondRound { - fn agg_nonce( - &self, - ) -> impl Future::Container> + Send { - async move { todo!() } + fn agg_nonce(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2SecondRoundAggNonce { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { + return Err(ClientError::ProtocolError(res)); + }; + AggNonce::from_bytes(&nonce).map_err(|_| ClientError::BadData) + } } fn holdouts( &self, ) -> impl Future::Container>> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2SecondRoundHoldouts { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { + return Err(ClientError::ProtocolError(res)); + }; + pubkeys + .into_iter() + .map(|pk| PublicKey::from_slice(&pk)) + .collect::, Error>>() + .map_err(|_| ClientError::BadData) + } } fn our_signature( &self, ) -> impl Future::Container> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2SecondRoundOurSignature { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { + return Err(ClientError::ProtocolError(res)); + }; + musig2::PartialSignature::from_slice(&sig).map_err(|_| ClientError::BadData) + } } fn is_complete(&self) -> impl Future::Container> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2SecondRoundIsComplete { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(complete) + } } fn receive_signature( @@ -148,7 +291,18 @@ impl Musig2SignerSecondRound for Musig2SecondRound { signature: musig2::PartialSignature, ) -> impl Future::Container>> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2SecondRoundReceiveSignature { + session_id: self.session_id, + pubkey: pubkey.serialize(), + signature: signature.serialize(), + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(maybe_err.map_or(Ok(()), Err)) + } } fn finalize( @@ -156,88 +310,157 @@ impl Musig2SignerSecondRound for Musig2SecondRound { ) -> impl Future< Output = ::Container>, > + Send { - async move { todo!() } + async move { + let msg = ClientMessage::Musig2SecondRoundFinalize { + session_id: self.session_id, + }; + let res = make_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundFinalize(res) = res else { + return Err(ClientError::ProtocolError(res)); + }; + let res: Result<_, _> = res.into(); + Ok(match res { + Ok(sig) => { + let sig = + LiftedSignature::from_bytes(&sig).map_err(|_| ClientError::BadData)?; + Ok(sig) + } + Err(e) => Err(e), + }) + } } } -impl SecretService for SecretServiceClient { - type OperatorSigner = OperatorClient; - - type P2PSigner = P2PClient; - - type Musig2Signer = Musig2Client; - - type WotsSigner = WotsClient; - - fn operator_signer(&self) -> Self::OperatorSigner { - OperatorClient(self.conn.as_ref().unwrap().clone()) - } - - fn p2p_signer(&self) -> Self::P2PSigner { - todo!() - } - - fn musig2_signer(&self) -> Self::Musig2Signer { - todo!() - } - - fn wots_signer(&self) -> Self::WotsSigner { - todo!() - } +struct OperatorClient { + conn: Connection, + config: Arc, } -struct OperatorClient(Connection); - impl OperatorSigner for OperatorClient { - type OperatorSigningError = (); - fn sign_psbt( &self, psbt: Psbt, - ) -> impl Future::Container>> - + Send { - async move { todo!() } + ) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::OperatorSignPsbt { + psbt: psbt.serialize(), + }; + let res = make_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::OperatorSignPsbt { psbt } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Psbt::deserialize(&psbt).map_err(|_| ClientError::BadData) + } } } -struct P2PClient(Connection); +struct P2PClient { + conn: Connection, + config: Arc, +} impl P2PSigner for P2PClient { - type P2PSigningError = (); - fn sign_p2p( &self, hash: [u8; 32], - ) -> impl Future::Container>> - + Send { - async move { todo!() } + ) -> impl Future::Container<[u8; 64]>> + Send { + async move { + let msg = ClientMessage::SignP2P { hash }; + let res = make_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::SignP2P { sig } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(sig) + } } - fn p2p_pubkey(&self) -> impl Future + Send { - async move { todo!() } + fn p2p_pubkey(&self) -> impl Future::Container<[u8; 33]>> + Send { + async move { + let msg = ClientMessage::P2PPubkey; + let res = make_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::P2PPubkey { pubkey } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(pubkey) + } } } -struct Musig2Client(Connection); +struct Musig2Client { + conn: Connection, + config: Arc, +} impl Musig2Signer for Musig2Client { - fn new_session( - &self, - ) -> impl Future::Container> + Send { + fn new_session(&self) -> impl Future> + Send { async move { - // self.0.open_bi(); - todo!() + let msg = ClientMessage::Musig2NewSession; + let res = make_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2NewSession { session_id } = res else { + return Err(ClientError::ProtocolError(res)); + }; + + Ok(Musig2FirstRound { + session_id, + connection: self.conn.clone(), + config: self.config.clone(), + }) } } } -struct WotsClient(Connection); +struct WotsClient { + conn: Connection, + config: Arc, +} impl WotsSigner for WotsClient { fn get_key( &self, index: u64, ) -> impl Future::Container<[u8; 64]>> + Send { - async move { todo!() } + async move { + let msg = ClientMessage::WotsGetKey { index }; + let res = make_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::WotsGetKey { key } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(key) + } } } + +async fn make_req( + conn: &Connection, + msg: ClientMessage, + timeout_dur: Duration, +) -> Result { + let (mut tx, mut rx) = conn.open_bi().await.map_err(ClientError::ConnectionError)?; + timeout( + timeout_dur, + tx.write_all(&msg.serialize().map_err(ClientError::SerializationError)?), + ) + .await + .map_err(|_| ClientError::Timeout)? + .map_err(ClientError::WriteError)?; + + let len_to_read = { + let mut buf = [0; size_of::()]; + timeout(timeout_dur, rx.read_exact(&mut buf)) + .await + .map_err(|_| ClientError::Timeout)? + .map_err(ClientError::ReadError)?; + LengthUint::from_le_bytes(buf) + }; + + let mut buf = vec![0; len_to_read as usize]; + timeout(timeout_dur, rx.read_exact(&mut buf)) + .await + .map_err(|_| ClientError::Timeout)? + .map_err(ClientError::ReadError)?; + + let archived = rkyv::access::(&buf) + .map_err(ClientError::DeserializationError)?; + + Ok(deserialize(archived).map_err(ClientError::DeserializationError)?) +} diff --git a/crates/secret-service-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml index 837a87ac..03952a66 100644 --- a/crates/secret-service-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" [dependencies] bitcoin.workspace = true musig2.workspace = true +quinn.workspace = true rkyv.workspace = true diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 786ab66a..10b287f1 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, future::Future}; +use std::future::Future; use bitcoin::Psbt; use musig2::{ @@ -6,9 +6,10 @@ use musig2::{ secp256k1::PublicKey, AggNonce, LiftedSignature, PartialSignature, PubNonce, }; -use rkyv::{ - api::high::HighSerializer, rancor, ser::allocator::ArenaHandle, util::AlignedVec, Serialize, -}; +use quinn::{ConnectionError, ReadExactError, WriteError}; +use rkyv::rancor; + +use super::wire::ServerMessage; pub trait SecretServiceFactory: Send + Clone where @@ -40,29 +41,23 @@ where } pub trait OperatorSigner: Send { - type OperatorSigningError: Debug - + Send - + Clone - + for<'a> Serialize, rancor::Error>>; + // type OperatorSigningError: Debug + // + Send + // + Clone + // + for<'a> Serialize, rancor::Error>>; - fn sign_psbt( - &self, - psbt: Psbt, - ) -> impl Future>> + Send; + fn sign_psbt(&self, psbt: Psbt) -> impl Future> + Send; } pub trait P2PSigner: Send { - type P2PSigningError: Debug - + Send - + Clone - + for<'a> Serialize, rancor::Error>>; + // type P2PSigningError: Debug + // + Send + // + Clone + // + for<'a> Serialize, rancor::Error>>; - fn sign_p2p( - &self, - hash: [u8; 32], - ) -> impl Future>> + Send; + fn sign_p2p(&self, hash: [u8; 32]) -> impl Future> + Send; - fn p2p_pubkey(&self) -> impl Future + Send; + fn p2p_pubkey(&self) -> impl Future> + Send; } pub type Musig2SessionId = usize; @@ -126,7 +121,16 @@ impl Origin for Server { pub struct Client; impl Origin for Client { - type Container = Result; + type Container = Result; } -pub enum NetworkError {} +pub enum ClientError { + ConnectionError(ConnectionError), + SerializationError(rancor::Error), + DeserializationError(rancor::Error), + BadData, + WriteError(WriteError), + ReadError(ReadExactError), + Timeout, + ProtocolError(ServerMessage), +} diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 983a7f01..ee85f983 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -8,9 +8,7 @@ use rkyv::{ Archive, Deserialize, Serialize, }; -use super::traits::{ - Musig2SessionId, Musig2SignerFirstRound, OperatorSigner, P2PSigner, SecretService, Server, -}; +use super::traits::Musig2SessionId; trait WireMessageMarker: for<'a> Serialize, rancor::Error>> @@ -18,25 +16,34 @@ trait WireMessageMarker: } #[derive(Debug, Clone, Archive, Serialize, Deserialize)] -pub enum ServerMessage -where - S: SecretService, - FirstRound: Musig2SignerFirstRound, -{ +pub enum ServerMessage { InvalidClientMessage, OpaqueServerError, - OperatorSignPsbt( - Result, >::OperatorSigningError>, - ), + OperatorSignPsbt { + psbt: Vec, + }, - SignP2P(Result<[u8; 64], >::P2PSigningError>), + SignP2P { + sig: [u8; 64], + }, + P2PPubkey { + pubkey: [u8; 33], + }, - Musig2NewSession(Musig2SessionId), + Musig2NewSession { + session_id: Musig2SessionId, + }, - Musig2FirstRoundOurNonce([u8; 66]), - Musig2FirstRoundHoldouts(Vec<[u8; 33]>), - Musig2FirstRoundIsComplete(bool), + Musig2FirstRoundOurNonce { + our_nonce: [u8; 66], + }, + Musig2FirstRoundHoldouts { + pubkeys: Vec<[u8; 33]>, + }, + Musig2FirstRoundIsComplete { + complete: bool, + }, Musig2FirstRoundReceivePubNonce( #[rkyv(with = Map)] Option, @@ -45,25 +52,30 @@ where #[rkyv(with = Map)] Option, ), - Musig2SecondRoundAggNonce([u8; 66]), - Musig2SecondRoundHoldouts(Vec<[u8; 33]>), - Musig2SecondRoundOurSignature([u8; 32]), - Musig2SecondRoundIsComplete(bool), + Musig2SecondRoundAggNonce { + nonce: [u8; 66], + }, + Musig2SecondRoundHoldouts { + pubkeys: Vec<[u8; 33]>, + }, + Musig2SecondRoundOurSignature { + sig: [u8; 32], + }, + Musig2SecondRoundIsComplete { + complete: bool, + }, Musig2SecondRoundReceiveSignature( #[rkyv(with = Map)] Option, ), Musig2SecondRoundFinalize(Musig2SessionResult), - WotsGetKey([u8; 64]), + WotsGetKey { + key: [u8; 64], + }, } -impl WireMessageMarker for ServerMessage -where - S: SecretService, - FirstRound: Musig2SignerFirstRound, -{ -} +impl WireMessageMarker for ServerMessage {} #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum Musig2SessionResult { @@ -100,6 +112,7 @@ pub enum ClientMessage { SignP2P { hash: [u8; 32], }, + P2PPubkey, Musig2NewSession, @@ -154,15 +167,17 @@ pub trait WireMessage { fn serialize(&self) -> Result; } +pub type LengthUint = u16; + // ignore, probably will just directly write to the connection instead of this impl WireMessage for T { fn serialize(&self) -> Result { let mut aligned_buf = AlignedVec::new(); - aligned_buf.extend_from_slice(&u32::MAX.to_le_bytes()); + aligned_buf.extend_from_slice(&LengthUint::MAX.to_le_bytes()); let mut aligned_buf = to_bytes_in(self, aligned_buf)?; - let len = aligned_buf.len() - size_of::(); - assert!(len <= u32::MAX as usize); - (len as u32) + let len = aligned_buf.len() - size_of::(); + assert!(len <= LengthUint::MAX as usize); + (len as LengthUint) .to_le_bytes() .into_iter() .enumerate() diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index d75b5dd3..7711487c 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -12,23 +12,21 @@ use std::{ }; use bitcoin::{secp256k1::PublicKey, Psbt}; -use kanal::AsyncSender; use ms2sm::Musig2SessionManager; use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; pub use quinn::rustls; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, ConnectionError, Endpoint, Incoming, ReadExactError, RecvStream, SendStream, ServerConfig, - WriteError, }; -use rkyv::rancor::{self, Error}; +use rkyv::rancor::Error; use secret_service_proto::{ v1::{ traits::{ Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, P2PSigner, SecretService, Server, WotsSigner, }, - wire::{ArchivedClientMessage, ServerMessage, WireMessage}, + wire::{ArchivedClientMessage, LengthUint, ServerMessage, WireMessage}, }, wire::ArchivedVersionedClientMessage, }; @@ -139,14 +137,10 @@ async fn conn_handler( } } -async fn request_manager( +async fn request_manager( mut tx: SendStream, - handler: JoinHandle, ReadExactError>>, -) where - FirstRound: Musig2SignerFirstRound, - SecondRound: Musig2SignerSecondRound, - Service: SecretService, -{ + handler: JoinHandle>, +) { let handler_res = match handler.await { Ok(r) => r, Err(e) => { @@ -176,16 +170,16 @@ async fn request_handler( mut rx: RecvStream, service: Arc, musig2_sm: Arc>>, -) -> Result, ReadExactError> +) -> Result where FirstRound: Musig2SignerFirstRound, SecondRound: Musig2SignerSecondRound, Service: SecretService, { let len_to_read = { - let mut buf = 0u16.to_le_bytes(); + let mut buf = [0; size_of::()]; rx.read_exact(&mut buf).await?; - u16::from_le_bytes(buf) + LengthUint::from_le_bytes(buf) }; let mut buf = vec![0u8; len_to_read as usize]; @@ -197,19 +191,26 @@ where ArchivedVersionedClientMessage::V1(req) => match req { ArchivedClientMessage::OperatorSignPsbt { psbt } => { let psbt = Psbt::deserialize(&psbt).unwrap(); - let r = service.operator_signer().sign_psbt(psbt).await; - ServerMessage::OperatorSignPsbt(r.map(|psbt| psbt.serialize())) + let psbt = service.operator_signer().sign_psbt(psbt).await; + ServerMessage::OperatorSignPsbt { + psbt: psbt.serialize(), + } } ArchivedClientMessage::SignP2P { hash } => { - let r = service.p2p_signer().sign_p2p(*hash).await; - ServerMessage::SignP2P(r) + let sig = service.p2p_signer().sign_p2p(*hash).await; + ServerMessage::SignP2P { sig } + } + + ArchivedClientMessage::P2PPubkey => { + let pubkey = service.p2p_signer().p2p_pubkey().await; + ServerMessage::P2PPubkey { pubkey } } ArchivedClientMessage::Musig2NewSession => { let first_round = service.musig2_signer().new_session().await; match musig2_sm.lock().await.new_session(first_round) { - Some(id) => ServerMessage::Musig2NewSession(id), + Some(session_id) => ServerMessage::Musig2NewSession { session_id }, None => ServerMessage::OpaqueServerError, } } @@ -220,8 +221,8 @@ where .first_round(session_id.to_native() as usize); match r { Ok(Some(first_round)) => { - let nonce = first_round.our_nonce().await.serialize(); - ServerMessage::Musig2FirstRoundOurNonce(nonce) + let our_nonce = first_round.our_nonce().await.serialize(); + ServerMessage::Musig2FirstRoundOurNonce { our_nonce } } _ => ServerMessage::InvalidClientMessage, } @@ -232,14 +233,14 @@ where .await .first_round(session_id.to_native() as usize); match r { - Ok(Some(first_round)) => ServerMessage::Musig2FirstRoundHoldouts( - first_round + Ok(Some(first_round)) => ServerMessage::Musig2FirstRoundHoldouts { + pubkeys: first_round .holdouts() .await .iter() .map(PublicKey::serialize) .collect(), - ), + }, _ => ServerMessage::InvalidClientMessage, } } @@ -249,9 +250,9 @@ where .await .first_round(session_id.to_native() as usize); match r { - Ok(Some(first_round)) => { - ServerMessage::Musig2FirstRoundIsComplete(first_round.is_complete().await) - } + Ok(Some(first_round)) => ServerMessage::Musig2FirstRoundIsComplete { + complete: first_round.is_complete().await, + }, _ => ServerMessage::InvalidClientMessage, } } @@ -303,9 +304,9 @@ where .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => { - ServerMessage::Musig2SecondRoundAggNonce(sr.agg_nonce().await.serialize()) - } + Ok(Some(sr)) => ServerMessage::Musig2SecondRoundAggNonce { + nonce: sr.agg_nonce().await.serialize(), + }, _ => ServerMessage::InvalidClientMessage, } } @@ -316,13 +317,14 @@ where .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => ServerMessage::Musig2SecondRoundHoldouts( - sr.holdouts() + Ok(Some(sr)) => ServerMessage::Musig2SecondRoundHoldouts { + pubkeys: sr + .holdouts() .await .iter() .map(PublicKey::serialize) .collect(), - ), + }, _ => ServerMessage::InvalidClientMessage, } } @@ -333,9 +335,9 @@ where .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => ServerMessage::Musig2SecondRoundOurSignature( - sr.our_signature().await.serialize(), - ), + Ok(Some(sr)) => ServerMessage::Musig2SecondRoundOurSignature { + sig: sr.our_signature().await.serialize(), + }, _ => ServerMessage::InvalidClientMessage, } } @@ -346,9 +348,9 @@ where .second_round(session_id.to_native() as usize); match sr { - Ok(Some(sr)) => { - ServerMessage::Musig2SecondRoundIsComplete(sr.is_complete().await) - } + Ok(Some(sr)) => ServerMessage::Musig2SecondRoundIsComplete { + complete: sr.is_complete().await, + }, _ => ServerMessage::InvalidClientMessage, } } @@ -390,26 +392,9 @@ where } ArchivedClientMessage::WotsGetKey { index } => { - let r = service.wots_signer().get_key(index.into()).await; - ServerMessage::WotsGetKey(r) + let key = service.wots_signer().get_key(index.into()).await; + ServerMessage::WotsGetKey { key } } }, }) } - -enum IoError { - WriteError(WriteError), - ReadError(ReadExactError), -} - -impl From for IoError { - fn from(e: WriteError) -> Self { - IoError::WriteError(e) - } -} - -impl From for IoError { - fn from(e: ReadExactError) -> Self { - IoError::ReadError(e) - } -} From 07514c3fb33c130a82564f9286f52b0ef1d1e8af Mon Sep 17 00:00:00 2001 From: Azz Date: Wed, 29 Jan 2025 12:56:56 +0000 Subject: [PATCH 06/30] use versioned properly --- crates/secret-service-client/src/lib.rs | 63 ++++++++++++-------- crates/secret-service-proto/src/lib.rs | 15 ----- crates/secret-service-proto/src/v1/traits.rs | 1 + crates/secret-service-proto/src/v1/wire.rs | 43 +------------ crates/secret-service-proto/src/wire.rs | 47 ++++++++++++++- crates/secret-service-server/src/lib.rs | 6 +- 6 files changed, 89 insertions(+), 86 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 3e482a88..ca332409 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -17,12 +17,18 @@ use quinn::{ rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, }; use rkyv::{deserialize, rancor}; -use secret_service_proto::v1::{ - traits::{ - Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, - Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, WotsSigner, +use secret_service_proto::{ + v1::{ + traits::{ + Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, + Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, WotsSigner, + }, + wire::{ClientMessage, ServerMessage}, + }, + wire::{ + ArchivedVersionedServerMessage, LengthUint, VersionedClientMessage, VersionedServerMessage, + WireMessage, }, - wire::{ArchivedServerMessage, ClientMessage, LengthUint, ServerMessage, WireMessage}, }; use terrors::OneOf; use tokio::time::timeout; @@ -131,7 +137,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { let msg = ClientMessage::Musig2FirstRoundOurNonce { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -146,7 +152,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { let msg = ClientMessage::Musig2FirstRoundHoldouts { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -163,7 +169,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { let msg = ClientMessage::Musig2FirstRoundIsComplete { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -183,7 +189,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { pubkey: pubkey.serialize(), pubnonce: pubnonce.serialize(), }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { return Err(ClientError::ProtocolError(res)); }; @@ -202,7 +208,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { session_id: self.session_id, hash, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { return Err(ClientError::ProtocolError(res)); }; @@ -230,7 +236,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { let msg = ClientMessage::Musig2SecondRoundAggNonce { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -245,7 +251,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { let msg = ClientMessage::Musig2SecondRoundHoldouts { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -264,7 +270,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { let msg = ClientMessage::Musig2SecondRoundOurSignature { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -277,7 +283,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { let msg = ClientMessage::Musig2SecondRoundIsComplete { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -297,7 +303,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { pubkey: pubkey.serialize(), signature: signature.serialize(), }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { return Err(ClientError::ProtocolError(res)); }; @@ -314,7 +320,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { let msg = ClientMessage::Musig2SecondRoundFinalize { session_id: self.session_id, }; - let res = make_req(&self.connection, msg, self.config.timeout).await?; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundFinalize(res) = res else { return Err(ClientError::ProtocolError(res)); }; @@ -345,7 +351,7 @@ impl OperatorSigner for OperatorClient { let msg = ClientMessage::OperatorSignPsbt { psbt: psbt.serialize(), }; - let res = make_req(&self.conn, msg, self.config.timeout).await?; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::OperatorSignPsbt { psbt } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -366,7 +372,7 @@ impl P2PSigner for P2PClient { ) -> impl Future::Container<[u8; 64]>> + Send { async move { let msg = ClientMessage::SignP2P { hash }; - let res = make_req(&self.conn, msg, self.config.timeout).await?; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::SignP2P { sig } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -377,7 +383,7 @@ impl P2PSigner for P2PClient { fn p2p_pubkey(&self) -> impl Future::Container<[u8; 33]>> + Send { async move { let msg = ClientMessage::P2PPubkey; - let res = make_req(&self.conn, msg, self.config.timeout).await?; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::P2PPubkey { pubkey } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -395,7 +401,7 @@ impl Musig2Signer for Musig2Client { fn new_session(&self) -> impl Future> + Send { async move { let msg = ClientMessage::Musig2NewSession; - let res = make_req(&self.conn, msg, self.config.timeout).await?; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2NewSession { session_id } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -421,7 +427,7 @@ impl WotsSigner for WotsClient { ) -> impl Future::Container<[u8; 64]>> + Send { async move { let msg = ClientMessage::WotsGetKey { index }; - let res = make_req(&self.conn, msg, self.config.timeout).await?; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::WotsGetKey { key } = res else { return Err(ClientError::ProtocolError(res)); }; @@ -430,7 +436,7 @@ impl WotsSigner for WotsClient { } } -async fn make_req( +async fn make_v1_req( conn: &Connection, msg: ClientMessage, timeout_dur: Duration, @@ -438,7 +444,11 @@ async fn make_req( let (mut tx, mut rx) = conn.open_bi().await.map_err(ClientError::ConnectionError)?; timeout( timeout_dur, - tx.write_all(&msg.serialize().map_err(ClientError::SerializationError)?), + tx.write_all( + &VersionedClientMessage::V1(msg) + .serialize() + .map_err(ClientError::SerializationError)?, + ), ) .await .map_err(|_| ClientError::Timeout)? @@ -459,8 +469,11 @@ async fn make_req( .map_err(|_| ClientError::Timeout)? .map_err(ClientError::ReadError)?; - let archived = rkyv::access::(&buf) + let archived = rkyv::access::(&buf) .map_err(ClientError::DeserializationError)?; - Ok(deserialize(archived).map_err(ClientError::DeserializationError)?) + let VersionedServerMessage::V1(msg) = + deserialize(archived).map_err(ClientError::DeserializationError)?; + + Ok(msg) } diff --git a/crates/secret-service-proto/src/lib.rs b/crates/secret-service-proto/src/lib.rs index b6c97292..8f2d6bbc 100644 --- a/crates/secret-service-proto/src/lib.rs +++ b/crates/secret-service-proto/src/lib.rs @@ -1,17 +1,2 @@ pub mod v1; pub mod wire; - -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 10b287f1..2b1286b8 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -133,4 +133,5 @@ pub enum ClientError { ReadError(ReadExactError), Timeout, ProtocolError(ServerMessage), + WrongVersion, } diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index ee85f983..d3d3f68f 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -1,20 +1,8 @@ use musig2::errors::{RoundContributionError, RoundFinalizeError}; -use rkyv::{ - api::high::{to_bytes_in, HighSerializer}, - rancor, - ser::allocator::ArenaHandle, - util::AlignedVec, - with::Map, - Archive, Deserialize, Serialize, -}; +use rkyv::{with::Map, Archive, Deserialize, Serialize}; use super::traits::Musig2SessionId; -trait WireMessageMarker: - for<'a> Serialize, rancor::Error>> -{ -} - #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum ServerMessage { InvalidClientMessage, @@ -75,8 +63,6 @@ pub enum ServerMessage { }, } -impl WireMessageMarker for ServerMessage {} - #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum Musig2SessionResult { Ok([u8; 64]), @@ -160,30 +146,3 @@ pub enum ClientMessage { index: u64, }, } - -impl WireMessageMarker for ClientMessage {} - -pub trait WireMessage { - fn serialize(&self) -> Result; -} - -pub type LengthUint = u16; - -// ignore, probably will just directly write to the connection instead of this -impl WireMessage for T { - fn serialize(&self) -> Result { - let mut aligned_buf = AlignedVec::new(); - aligned_buf.extend_from_slice(&LengthUint::MAX.to_le_bytes()); - let mut aligned_buf = to_bytes_in(self, aligned_buf)?; - let len = aligned_buf.len() - size_of::(); - assert!(len <= LengthUint::MAX as usize); - (len as LengthUint) - .to_le_bytes() - .into_iter() - .enumerate() - .for_each(|byte| { - aligned_buf[byte.0] = byte.1; - }); - Ok(aligned_buf) - } -} diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs index bf54d288..3e3d9f5b 100644 --- a/crates/secret-service-proto/src/wire.rs +++ b/crates/secret-service-proto/src/wire.rs @@ -1,9 +1,54 @@ -use rkyv::{Archive, Deserialize, Serialize}; +use rkyv::{ + api::high::{to_bytes_in, HighSerializer}, + rancor, + ser::allocator::ArenaHandle, + util::AlignedVec, + Archive, Deserialize, Serialize, +}; use crate::v1; +trait WireMessageMarker: + for<'a> Serialize, rancor::Error>> +{ +} + +pub trait WireMessage { + fn serialize(&self) -> Result; +} + +pub type LengthUint = u16; + #[repr(u8)] #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum VersionedClientMessage { V1(v1::wire::ClientMessage), } + +impl WireMessageMarker for VersionedClientMessage {} + +#[repr(u8)] +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum VersionedServerMessage { + V1(v1::wire::ServerMessage), +} + +impl WireMessageMarker for VersionedServerMessage {} + +impl WireMessage for T { + fn serialize(&self) -> Result { + let mut aligned_buf = AlignedVec::new(); + aligned_buf.extend_from_slice(&LengthUint::MAX.to_le_bytes()); + let mut aligned_buf = to_bytes_in(self, aligned_buf)?; + let len = aligned_buf.len() - size_of::(); + assert!(len <= LengthUint::MAX as usize); + (len as LengthUint) + .to_le_bytes() + .into_iter() + .enumerate() + .for_each(|byte| { + aligned_buf[byte.0] = byte.1; + }); + Ok(aligned_buf) + } +} diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 7711487c..ed8ea30b 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -26,9 +26,9 @@ use secret_service_proto::{ Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, P2PSigner, SecretService, Server, WotsSigner, }, - wire::{ArchivedClientMessage, LengthUint, ServerMessage, WireMessage}, + wire::{ArchivedClientMessage, ServerMessage}, }, - wire::ArchivedVersionedClientMessage, + wire::{ArchivedVersionedClientMessage, LengthUint, VersionedServerMessage, WireMessage}, }; use terrors::OneOf; use tokio::{ @@ -151,7 +151,7 @@ async fn request_manager( match handler_res { Ok(msg) => { - let byte_response = match WireMessage::serialize(&msg) { + let byte_response = match WireMessage::serialize(&VersionedServerMessage::V1(msg)) { Ok(r) => r, Err(e) => { error!("failed to serialize response: {e:?}"); From 04644a539d2cce516509321d14de40fa67cf6b16 Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 3 Feb 2025 12:34:27 +0000 Subject: [PATCH 07/30] serializable musig2 rounds, implementation --- Cargo.toml | 9 +- crates/musig2/.gitignore | 3 + crates/musig2/Cargo.toml | 52 + crates/musig2/LICENSE | 24 + crates/musig2/Makefile | 44 + crates/musig2/README.md | 49 + crates/musig2/doc/API.md | 742 +++++++++++++ crates/musig2/doc/adaptor_signatures.md | 246 +++++ crates/musig2/reference.py | 882 ++++++++++++++++ crates/musig2/src/binary_encoding.rs | 295 ++++++ crates/musig2/src/bip340.rs | 445 ++++++++ crates/musig2/src/deterministic.rs | 147 +++ crates/musig2/src/errors.rs | 386 +++++++ crates/musig2/src/key_agg.rs | 988 +++++++++++++++++ crates/musig2/src/key_sort.rs | 22 + crates/musig2/src/lib.rs | 55 + crates/musig2/src/nonces.rs | 992 ++++++++++++++++++ crates/musig2/src/rkyv_wrappers.rs | 190 ++++ crates/musig2/src/rounds.rs | 724 +++++++++++++ crates/musig2/src/sig_agg.rs | 283 +++++ crates/musig2/src/signature.rs | 432 ++++++++ crates/musig2/src/signing.rs | 615 +++++++++++ crates/musig2/src/tagged_hashes.rs | 208 ++++ .../src/test_vectors/bip340_vectors.csv | 20 + .../src/test_vectors/key_agg_vectors.json | 88 ++ .../src/test_vectors/key_sort_vectors.json | 18 + .../src/test_vectors/nonce_agg_vectors.json | 51 + .../src/test_vectors/nonce_gen_vectors.json | 34 + .../src/test_vectors/sig_agg_vectors.json | 151 +++ .../src/test_vectors/sign_verify_vectors.json | 212 ++++ .../src/test_vectors/tweak_vectors.json | 84 ++ crates/musig2/src/testhex.rs | 93 ++ .../tests/fuzz_against_reference_impl.rs | 317 ++++++ crates/secret-service-proto/src/v1/traits.rs | 5 + crates/secret-service-server/src/lib.rs | 6 +- crates/secret-service-server/src/ms2sm.rs | 6 +- crates/secret-service/Cargo.toml | 7 + crates/secret-service/src/main.rs | 211 +++- 38 files changed, 9125 insertions(+), 11 deletions(-) create mode 100644 crates/musig2/.gitignore create mode 100644 crates/musig2/Cargo.toml create mode 100644 crates/musig2/LICENSE create mode 100644 crates/musig2/Makefile create mode 100644 crates/musig2/README.md create mode 100644 crates/musig2/doc/API.md create mode 100644 crates/musig2/doc/adaptor_signatures.md create mode 100644 crates/musig2/reference.py create mode 100644 crates/musig2/src/binary_encoding.rs create mode 100644 crates/musig2/src/bip340.rs create mode 100644 crates/musig2/src/deterministic.rs create mode 100644 crates/musig2/src/errors.rs create mode 100644 crates/musig2/src/key_agg.rs create mode 100644 crates/musig2/src/key_sort.rs create mode 100644 crates/musig2/src/lib.rs create mode 100644 crates/musig2/src/nonces.rs create mode 100644 crates/musig2/src/rkyv_wrappers.rs create mode 100644 crates/musig2/src/rounds.rs create mode 100644 crates/musig2/src/sig_agg.rs create mode 100644 crates/musig2/src/signature.rs create mode 100644 crates/musig2/src/signing.rs create mode 100644 crates/musig2/src/tagged_hashes.rs create mode 100644 crates/musig2/src/test_vectors/bip340_vectors.csv create mode 100644 crates/musig2/src/test_vectors/key_agg_vectors.json create mode 100644 crates/musig2/src/test_vectors/key_sort_vectors.json create mode 100644 crates/musig2/src/test_vectors/nonce_agg_vectors.json create mode 100644 crates/musig2/src/test_vectors/nonce_gen_vectors.json create mode 100644 crates/musig2/src/test_vectors/sig_agg_vectors.json create mode 100644 crates/musig2/src/test_vectors/sign_verify_vectors.json create mode 100644 crates/musig2/src/test_vectors/tweak_vectors.json create mode 100644 crates/musig2/src/testhex.rs create mode 100644 crates/musig2/tests/fuzz_against_reference_impl.rs diff --git a/Cargo.toml b/Cargo.toml index 59e3d009..f4cde209 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,11 @@ members = [ "crates/tx-graph", "crates/btc-notify", "bridge-guest-builder", + "crates/secret-service", + "crates/secret-service-proto", + "crates/secret-service-client", + "crates/secret-service-server", + "crates/musig2", # binaries listed separately "bin/strata-bridge", @@ -20,10 +25,6 @@ members = [ # test utilities "crates/test-utils", - "crates/secret-service", - "crates/secret-service-proto", - "crates/secret-service-client", - "crates/secret-service-server", ] default-members = ["bin/strata-bridge", "bin/dev-cli", "bin/assert-splitter"] diff --git a/crates/musig2/.gitignore b/crates/musig2/.gitignore new file mode 100644 index 00000000..6430f5e0 --- /dev/null +++ b/crates/musig2/.gitignore @@ -0,0 +1,3 @@ +/target +__pycache__ +/Cargo.lock diff --git a/crates/musig2/Cargo.toml b/crates/musig2/Cargo.toml new file mode 100644 index 00000000..dd612494 --- /dev/null +++ b/crates/musig2/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "musig2" +version = "0.1.0" +edition = "2021" +authors = ["conduition "] +description = "Flexible Rust implementation of the MuSig2 multisignature protocol, compatible with Bitcoin." +readme = "README.md" +license = "Unlicense" +repository = "https://github.com/conduition/musig2" +keywords = ["musig", "schnorr", "bitcoin", "multisignature", "musig2"] +include = ["/src", "!/src/test_vectors", "*.md"] + +[dependencies] +base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } +hmac = { version = "0.12.1", default-features = false, features = [] } +k256 = { version = "0.13.1", default-features = false, optional = true } +once_cell = { version = "1.18.0", default-features = false } +rand = { version = "0.8.5", optional = true, default-features = false, features = [ + "std_rng", +] } +rkyv.workspace = true +secp = { version = "0.3", default-features = false } +secp256k1 = { version = "0.29", optional = true, default-features = false } +serde = { version = "1.0.188", default-features = false, optional = true } +serdect = { version = "0.2.0", default-features = false, optional = true, features = [ + "alloc", +] } +sha2 = { version = "0.10.8", default-features = false } +subtle = { version = "2.5.0", default-features = false } + +[dev-dependencies] +serde = { version = "1.0.188", features = ["serde_derive"] } +serde_json = "1.0.107" +csv = "1.3.0" +serdect = "0.2.0" +rand = "0.8.5" +secp = { version = "0.3", default-features = false, features = [ + "serde", + "rand", + "secp256k1-invert", +] } + +[features] +default = ["secp256k1"] +secp256k1 = ["dep:secp256k1", "secp/secp256k1"] +# k256 = ["dep:k256", "secp/k256"] +serde = ["dep:serde", "secp/serde", "dep:serdect"] +rand = ["dep:rand", "secp/rand"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/musig2/LICENSE b/crates/musig2/LICENSE new file mode 100644 index 00000000..fdddb29a --- /dev/null +++ b/crates/musig2/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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 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. + +For more information, please refer to diff --git a/crates/musig2/Makefile b/crates/musig2/Makefile new file mode 100644 index 00000000..e38deb74 --- /dev/null +++ b/crates/musig2/Makefile @@ -0,0 +1,44 @@ +.PHONY: check check-* test test-* +check: check-default check-mixed check-secp256k1 check-k256 + +# Checks the source code with default features enabled. +check-default: + cargo clippy + +# Checks the source code with all features enabled. +check-mixed: + cargo clippy --all-features + cargo clippy --all-features --tests + +# Checks the source code with variations of libsecp256k1 feature sets. +check-secp256k1: + cargo clippy --no-default-features --features secp256k1 + cargo clippy --no-default-features --features secp256k1,serde + cargo clippy --no-default-features --features secp256k1,serde,rand + cargo clippy --no-default-features --features secp256k1,serde,rand --tests + +# Checks the source code with variations of pure-rust feature sets. +check-k256: + cargo clippy --no-default-features --features k256 + cargo clippy --no-default-features --features k256,serde + cargo clippy --no-default-features --features k256,serde,rand + cargo clippy --no-default-features --features k256,serde,rand --tests + + +test: test-default test-mixed test-secp256k1 test-k256 + +test-default: + cargo test + +test-mixed: + cargo test --all-features + +test-secp256k1: + cargo test --no-default-features --features secp256k1,serde,rand + +test-k256: + cargo test --no-default-features --features k256,serde,rand + +.PHONY: docwatch +docwatch: + watch -n 5 cargo doc --all-features diff --git a/crates/musig2/README.md b/crates/musig2/README.md new file mode 100644 index 00000000..5a71ed0f --- /dev/null +++ b/crates/musig2/README.md @@ -0,0 +1,49 @@ +# MuSig2 + +This crate provides a flexible rust implementation of [MuSig2](https://eprint.iacr.org/2020/1261), an optimized digital signature aggregation protocol, on the `secp256k1` elliptic curve. + +MuSig2 allows groups of mutually distrusting parties to cooperatively sign data and aggregate their signatures into a single aggregated signature which is indistinguishable from a signature made by a single private key. The group collectively controls an _aggregated public key_ which can only create signatures if everyone in the group cooperates (AKA an N-of-N multisignature scheme). MuSig2 is optimized to support secure signature aggregation with only **two round-trips of network communication.** + +Specifically, this crate implements [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki), for creating and verifying signatures which validate under Bitcoin consensus rules, but the protocol is flexible and can be applied to any N-of-N multisignature use-case. + +## ⚠️ Beta Status ⚠️ + +This crate is in beta status. The latest release is a `v0.0.x` version number. Expect breaking changes and security fixes. Once this crate is stabilized, we will tag and release `v1.0.0`. + +## Overview + +If you're not already familiar with MuSig2, the process of cooperative signing runs like so: + +1. All signers share their public keys with one-another. The group computes an _aggregated public key_ which they collectively control. +2. In the **first signing round,** signers generate and share _nonces_ (random numbers) with one-another. These nonces have both secret and public versions. Only the public nonce (AKA `PubNonce`) should be shared, while the corresponding secret nonce (AKA `SecNonce`) must be kept secret. +3. Once every signer has received the public nonces of every other signer, each signer makes a _partial signature_ for a message using their secret key and secret nonce. +4. In the **second signing round,** signers share their partial signatures with one-another. Partial signatures can be verified to place blame on misbehaving signers. +5. A valid set of partial signatures can be aggregated into a final signature, which is just a normal [Schnorr signature](https://en.wikipedia.org/wiki/Schnorr_signature), valid under the aggregated public key. + +## Choice of Backbone + +This crate does not implement elliptic curve point math directly. Instead we depend on one of two reputable libraries: + +- C bindings to [`libsecp256k1`](https://github.com/bitcoin-core/secp256k1), via [the `secp256k1` crate](https://crates.io/crates/secp256k1), maintained by the Bitcoin Core team. +- A pure-rust implementation via [the `k256` crate](https://crates.io/crates/k256), maintained by the [RustCrypto](https://github.com/RustCrypto) team. + +One or the other can be used. By default, this crate prefers to rely on `libsecp256k1`, as this is the most vetted and publicly trusted implementation of secp256k1 curve math available anywhere. However, if you need a pure-rust implementation, you can install this crate without it, and use the pure-rust `k256` crate instead. + +```notrust +cargo add musig2 --no-default-features --features k256 +``` + +If both `k256` and `secp256k1` features are enabled, then we default to using `libsecp256k1` bindings for the actual math, but still provide trait implementations to make this crate interoperable with `k256`. + +This crate internally represents elliptic curve points (e.g. public keys) and scalars (e.g. private keys) using the [`secp` crate](https://crates.io/crates/secp) and its types: + +- [`secp::Scalar`](https://docs.rs/secp/struct.Scalar.html) for non-zero scalar values. +- [`secp::Point`](https://docs.rs/secp/struct.Point.html) for non-infinity curve points +- [`secp::MaybeScalar`](https://docs.rs/secp/enum.Point.html) for possibly-zero scalars. +- [`secp::MaybePoint`](https://docs.rs/secp/enum.Point.html) for possibly-infinity curve points. + +Depending on which features of this crate are enabled, conversion traits are implemented between these types and higher-level types such as [`secp256k1::PublicKey`](https://docs.rs/secp256k1/struct.PublicKey.html) or [`k256::SecretKey`](https://docs.rs/k256/type.SecretKey.html). Generally, our API can accept or return any type that converts to/from the equivalent `secp` representations, although callers are also welcome to use `secp` directly too. + +## Documentation + +[Head on over to docs.rs to see the full API documentation and usage examples.](https://docs.rs/musig2) diff --git a/crates/musig2/doc/API.md b/crates/musig2/doc/API.md new file mode 100644 index 00000000..36f22179 --- /dev/null +++ b/crates/musig2/doc/API.md @@ -0,0 +1,742 @@ +# Features + +| Feature | Description | Dependencies | Enabled by Default | +|---------|-------------|--------------|:------------------:| +| `secp256k1` | Use [`libsecp256k1`](https://github.com/bitcoin-core/secp256k1) bindings for elliptic curve math. Include trait implementations for converting to and from types in [the `secp256k1` crate][secp256k1]. This feature supercedes the `k256` feature if that one is enabled. | [`secp256k1`] | ✅ | +| `k256` | Use [the `k256` crate][k256] for elliptic curve math. This allows a pure-rust implementation of MuSig2. Include trait implementations for types from [`k256`]. If the `secp256k1` feature is enabled, then [`k256`] will still be brought in and trait implementations will be included, but the actual curve math will be done by `libsecp256k1`. | [`k256`] | ❌ | +| `serde` | Implement serialization and deserialization for types in this crate. | [`serde`](https://docs.rs/serde) | ❌ | +| `rand` | Enable support for accepting a CSPRNG as input, via [the `rand` crate][rand] | [`rand`] | ❌ | + +# Key Aggregation + +Once all signers know each other's public keys (out of scope for this crate), they can construct a [`KeyAggContext`] which aggregates their public keys together, along with optional _tweak values_ (see [`KeyAggContext::with_tweak`] to learn more). + + +
+

Example

+ +```rust +# #[cfg(feature = "secp256k1")] +use secp256k1::{SecretKey, PublicKey}; +# +# // k256::SecretKey and k256::PublicKey don't have string parsing traits, +# // so I'll just use our own representations for this example. +# #[cfg(not(feature = "secp256k1"))] +# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; +use musig2::KeyAggContext; + +let pubkeys = [ + "026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f4" + .parse::() + .unwrap(), + "02f3b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b" + .parse::() + .unwrap(), + "03204ea8bc3425b2cbc9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" + .parse::() + .unwrap(), +]; + +let signer_index = 2; +let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" + .parse() + .unwrap(); + +let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + + +// This is the key which the group has control over. +let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); +assert_eq!( + aggregated_pubkey, + "02e272de44ea720667aba55341a1a761c0fc8fbe294aa31dbaf1cff80f1c2fd940" + .parse() + .unwrap() +); +``` +
+
+ +A handy property of the MuSig2 protocol is that signers do not need proof that the other signers in the group know their own secret keys. They can simply exchange public keys and continue once all signers agree on an aggregated pubkey. + +Once you have a [`KeyAggContext`], you may choose between two sets of APIs for running the MuSig2 protocol, covering both **Functional** and **State-Machine** approaches. + +## State-Machine API + +A state machine is a stateful object which manipulates its internal state based on external input, fed to it by the caller (you). + +This crate's _State-Machine_-based signing API is safer, but may not be as flexible as the _Functional_ API. It is constructed around two stateful types, [`FirstRound`] and [`SecondRound`], which handle storing partial nonces and partial signatures. + +[`FirstRound`] is analagous to the first signing round of MuSig2, wherein signers generate and send nonces to one-another, or to a [designated aggregator](#single-aggregator). + +[`SecondRound`] is analagous to the second signing round where signers share and verify their partial signatures. Once the [`SecondRound`] complete, it can be finalized into a valid aggregated Schnorr signature. + + +
+

Example

+ +```rust +# #[cfg(feature = "secp256k1")] +# use secp256k1::{SecretKey, PublicKey}; +# #[cfg(not(feature = "secp256k1"))] +# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; +# use musig2::KeyAggContext; +# +# /// Same pubkeys as in previous example +# let key_agg_ctx = +# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ +# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ +# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" +# .parse::() +# .unwrap(); +# +# let signer_index = 2; +# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" +# .parse() +# .unwrap(); +# +# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); +# +use musig2::{ + CompactSignature, FirstRound, PartialSignature, PubNonce, SecNonceSpices, SecondRound, +}; + +// The group wants to sign something! +let message = "hello interwebz!"; + +// Normally this should be sampled securely from a CSPRNG. +// let mut nonce_seed = [0u8; 32] +// rand::rngs::OsRng.fill_bytes(&mut nonce_seed); +let nonce_seed = [0xACu8; 32]; + +let mut first_round = FirstRound::new( + key_agg_ctx, + nonce_seed, + signer_index, + SecNonceSpices::new() + .with_seckey(seckey) + .with_message(&message), +) +.unwrap(); + +// We would share our public nonce with our peers. +assert_eq!( + first_round.our_public_nonce(), + "02d1e90616ea78a612dddfe97de7b5e7e1ceef6e64b7bc23b922eae30fa2475cca\ + 02e676a3af322965d53cc128597897ef4f84a8d8080b456e27836db70e5343a2bb" + .parse() + .unwrap(), + "Our public nonce should match" +); + +// We can see a list of which signers (by index) have yet to provide us +// with a nonce. +assert_eq!(first_round.holdouts(), &[0, 1]); + +// We receive the public nonces from our peers one at a time. +first_round.receive_nonce( + 0, + "02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ + 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45" + .parse::() + .unwrap() +) +.unwrap(); + +// `is_complete` provides a quick check to see whether we have nonces from +// every signer yet. +assert!(!first_round.is_complete()); + +// ...once we receive all their nonces... +first_round.receive_nonce( + 1, + "020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ + 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d" + .parse::() + .unwrap() +) +.unwrap(); + +// ... the round will be complete. +assert!(first_round.is_complete()); + +let mut second_round: SecondRound<&str> = first_round.finalize(seckey, message).unwrap(); + +// We could now send our partial signature to our peers. +// Be careful not to send your signature first if your peers +// might run away without surrendering their signatures in exchange! +let our_partial_signature: PartialSignature = second_round.our_signature(); +assert_eq!( + our_partial_signature, + "efd62850b959a76a462f1e42eb3cecc77a5a0982742fff2901456b7d1453a817" + .parse() + .unwrap() +); + +second_round.receive_signature( + 0, + "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" + .parse::() + .unwrap() +) +.expect("signer 0's partial signature should be valid"); + +// Same methods as on FirstRound are available for SecondRound. +assert!(!second_round.is_complete()); +assert_eq!(second_round.holdouts(), &[1]); + +// Receive a partial signature from one of our cosigners. This +// automatically verifies the partial signature and returns an +// error if the signature is invalid. +second_round.receive_signature( + 1, + "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" + .parse::() + .unwrap() +) +.expect("signer 1's partial signature should be valid"); + +assert!(second_round.is_complete()); + +// If all signatures were received successfully, finalizing the second round +// should succeed with overwhelming probability. +let final_signature: CompactSignature = second_round.finalize().unwrap(); + +assert_eq!( + final_signature.to_string(), + "38fbd82d1d27bb3401042062acfd4e7f54ce93ddf26a4ae87cf71568c1d4e8bb\ + 8fca20bb6f7bce2c5b54576d315b21eae31a614641afd227cda221fd6b1c54ea" +); + +musig2::verify_single( + aggregated_pubkey, + final_signature, + message +) +.expect("aggregated signature must be valid"); +``` +
+
+ +## Functional API + +The _Functional_ API exposes the MuSig2 protocol through pure functions which accept read-only inputs and produce deterministic outputs. This obviously lacks internal state and it is thus entirely dependent on the caller to securely handle nonce state management. The caller is free to implement nonce state management however they like with this API. [Please read the warning below about nonce-reuse BEFORE attempting to use the Functional API](#nonce-reuse). + +Instead of using [`FirstRound`] and [`SecondRound`], the Functional API is exposed through these pure functions: + +- [`SecNonce::generate`] - Generate a secret nonce. +- [`AggNonce::sum`] - Aggregate public nonces together. +- [`sign_partial`] - Create a partial signature on a message. +- [`verify_partial`] - Verify a partial signature. +- [`aggregate_partial_signatures`] - Aggregate a collection of partial signatures into a final valid signature. + +
+

Example

+ +```rust +# #[cfg(feature = "secp256k1")] +# use secp256k1::{SecretKey, PublicKey}; +# #[cfg(not(feature = "secp256k1"))] +# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; +# use musig2::{KeyAggContext, PartialSignature, PubNonce}; +# +# let signer_index = 2; +# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" +# .parse() +# .unwrap(); +# +# /// Same pubkeys as in previous example +# let key_agg_ctx = +# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ +# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ +# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" +# .parse::() +# .unwrap(); +# +# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); +# let message = "hello interwebz!"; +# let nonce_seed = [0xACu8; 32]; +use musig2::{AggNonce, SecNonce}; + +// This is how `FirstRound` derives the nonce internally. +let secnonce = SecNonce::build(nonce_seed) + .with_seckey(seckey) + .with_message(&message) + .with_aggregated_pubkey(aggregated_pubkey) + .with_extra_input(&(signer_index as u32).to_be_bytes()) + .build(); + +let our_public_nonce = secnonce.public_nonce(); +assert_eq!( + our_public_nonce, + "02d1e90616ea78a612dddfe97de7b5e7e1ceef6e64b7bc23b922eae30fa2475cca\ + 02e676a3af322965d53cc128597897ef4f84a8d8080b456e27836db70e5343a2bb" + .parse() + .unwrap() +); + +// ...Exchange nonces with peers... + +let public_nonces = [ + "02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ + 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45" + .parse::() + .unwrap(), + + "020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ + 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d" + .parse::() + .unwrap(), + + our_public_nonce, +]; + +// We manually aggregate the nonces together and then construct our partial signature. +let aggregated_nonce: AggNonce = public_nonces.iter().sum(); +let our_partial_signature: PartialSignature = musig2::sign_partial( + &key_agg_ctx, + seckey, + secnonce, + &aggregated_nonce, + message +) +.expect("error creating partial signature"); + +let partial_signatures = [ + "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" + .parse::() + .unwrap(), + "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" + .parse::() + .unwrap(), + our_partial_signature, +]; + +/// Signatures should be verified upon receipt and invalid signatures +/// should be blamed on the signer who sent them. +for (i, partial_signature) in partial_signatures.into_iter().enumerate() { + if i == signer_index { + // Don't bother verifying our own signature + continue; + } + + let their_pubkey: PublicKey = key_agg_ctx.get_pubkey(i).unwrap(); + let their_pubnonce = &public_nonces[i]; + + musig2::verify_partial( + &key_agg_ctx, + partial_signature, + &aggregated_nonce, + their_pubkey, + their_pubnonce, + message + ) + .expect("received invalid signature from a peer"); +} + +let final_signature: [u8; 64] = musig2::aggregate_partial_signatures( + &key_agg_ctx, + &aggregated_nonce, + partial_signatures, + message, +) +.expect("error aggregating signatures"); + +assert_eq!( + final_signature, + [ + 0x38, 0xFB, 0xD8, 0x2D, 0x1D, 0x27, 0xBB, 0x34, 0x01, 0x04, 0x20, 0x62, 0xAC, 0xFD, + 0x4E, 0x7F, 0x54, 0xCE, 0x93, 0xDD, 0xF2, 0x6A, 0x4A, 0xE8, 0x7C, 0xF7, 0x15, 0x68, + 0xC1, 0xD4, 0xE8, 0xBB, 0x8F, 0xCA, 0x20, 0xBB, 0x6F, 0x7B, 0xCE, 0x2C, 0x5B, 0x54, + 0x57, 0x6D, 0x31, 0x5B, 0x21, 0xEA, 0xE3, 0x1A, 0x61, 0x46, 0x41, 0xAF, 0xD2, 0x27, + 0xCD, 0xA2, 0x21, 0xFD, 0x6B, 0x1C, 0x54, 0xEA + ] +); + +musig2::verify_single( + aggregated_pubkey, + &final_signature, + message +) +.expect("aggregated signature must be valid"); +``` +
+
+ +## Single Aggregator + +As an alternative to a many-to-many topology where each signer must collect nonces and partial signatures from everyone else in the group, the group can instead opt to nominate an _aggregator node_ whose duty is to collect nonces and signatures from all other signers, and then broadcast the aggregated signature once they receive all partial signatures. + +This dramatically decreases the number of network round-trips required for large groups of signers, and doesn't require any trust in the aggregator node beyond the possibility that they may refuse to reveal the final signature. + +Here's an example of how to use the State-Machine API to interact with an untrusted remote aggregator node. + +
+

Example

+ +```rust +# #[cfg(feature = "secp256k1")] +# use secp256k1::{SecretKey, PublicKey}; +# #[cfg(not(feature = "secp256k1"))] +# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; +# use musig2::KeyAggContext; +# +# /// Same pubkeys as in previous example +# let key_agg_ctx = +# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ +# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ +# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" +# .parse::() +# .unwrap(); +# +# let signer_index = 2; +# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" +# .parse() +# .unwrap(); +# +# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); +# +use musig2::{ + AggNonce, FirstRound, PartialSignature, PubNonce, SecNonceSpices, SecondRound, +}; + +let message = "hello interwebz!"; + +// Normally this should be sampled securely from a CSPRNG. +let nonce_seed = [0xACu8; 32]; + +let first_round = FirstRound::new( + key_agg_ctx.clone(), + nonce_seed, + signer_index, + SecNonceSpices::new() + .with_seckey(seckey) + .with_message(&message), +) +.unwrap(); + +// We would share our public nonce with the aggregator. +// The aggregator aggregates the group's nonces together +// and sends us the resulting `AggNonce`. +let aggregated_nonce = AggNonce::sum([ + "02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ + 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45" + .parse::() + .unwrap(), + + "020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ + 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d" + .parse::() + .unwrap(), + + first_round.our_public_nonce(), +]); + +// Once we have the aggregated nonce, we can sign the message, +// and send the partial signature to the aggregator. +let our_partial_signature = first_round + .sign_for_aggregator(seckey, message, &aggregated_nonce) + .unwrap(); + +let partial_signatures = [ + "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" + .parse::() + .unwrap(), + "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" + .parse::() + .unwrap(), + our_partial_signature, +]; + +// The aggregator aggregates the group's partial signatures, +// either using `SecondRound` or the functional API. +let final_signature: [u8; 64] = musig2::aggregate_partial_signatures( + &key_agg_ctx, + &aggregated_nonce, + partial_signatures, + message, +) +.unwrap(); + +musig2::verify_single( + aggregated_pubkey, + &final_signature, + message +) +.expect("aggregated signature must be valid"); +``` +
+
+ +The partial signatures can also be created using the functional API, as long as `SecNonce` is [managed carefully so that it is not accidentally reused.](#nonce-reuse) + +## Signatures + +Partial signatures are represented as a [`secp::MaybeScalar`], which is just a scalar in the range `[0, n)` (where `n` is the number of points on the curve). This is aliased as [`PartialSignature`] for clarity. `PartialSignature` implements `Serialize` and `Deserialize` if the `serde` feature is enabled. + +The final output of a signature aggregation is a tuple of numbers `(R, s)` where `R` is a point and `s` is a scalar. This output type is represented by the [`LiftedSignature`] type. The return value of [`SecondRound::finalize`] or [`aggregate_partial_signatures`] can be converted to any type that implements `From`. + +
+

Example

+ +```rust +# use musig2::{AggNonce, KeyAggContext, PartialSignature}; +# +# fn main() -> Result<(), Box> { +# /// Same pubkeys as in previous example +# let key_agg_ctx = +# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ +# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ +# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" +# .parse::() +# .unwrap(); +# let message = "hello interwebz!"; +# let partial_signatures = [ +# "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" +# .parse::() +# .unwrap(), +# "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" +# .parse::() +# .unwrap(), +# "efd62850b959a76a462f1e42eb3cecc77a5a0982742fff2901456b7d1453a817" +# .parse::() +# .unwrap(), +# ]; +# let aggregated_nonce = "03f9ce0458831f7f8104f014d940db4048c4e045c369c207ec38530360ce7bfd3e\ +# 023f5d6a34513458188503e7c48c1a6efd75f52e77da57587f372be8f839ecc1f9" +# .parse::() +# .unwrap(); +# +use musig2::{aggregate_partial_signatures, CompactSignature, LiftedSignature}; + +// Represents a compacted signature with an X-only nonce point. +let final_signature: CompactSignature = aggregate_partial_signatures( + // ... +# &key_agg_ctx, +# &aggregated_nonce, +# partial_signatures, +# message, +)?; + +// Represents a fully parsed `(R, s)` signature pair. +let final_signature: LiftedSignature = aggregate_partial_signatures( + // ... +# &key_agg_ctx, +# &aggregated_nonce, +# partial_signatures, +# message, +)?; + +// Or you can convert it directly to a byte array. +let final_signature: [u8; 64] = aggregate_partial_signatures( + // ... +# &key_agg_ctx, +# &aggregated_nonce, +# partial_signatures, +# message, +)?; + +# #[cfg(feature = "secp256k1")] +let final_signature: secp256k1::schnorr::Signature = aggregate_partial_signatures( + // ... +# &key_agg_ctx, +# &aggregated_nonce, +# partial_signatures, +# message, +)?; + +// allows us to use `R` as a variable name in this block +#[allow(non_snake_case)] +{ + // You can also unzip signatures into their individual components `(R, s)`. + let signature: LiftedSignature = aggregate_partial_signatures( + // ... + # &key_agg_ctx, + # &aggregated_nonce, + # partial_signatures, + # message, + )?; + + // `R` can be any type that impls `From`. + // `s` can be any type that impls `From`. + let (R, s): (secp::Point, secp::MaybeScalar) = signature.unzip(); + let (R, s): ([u8; 33], [u8; 32]) = signature.unzip(); + # #[cfg(feature = "secp256k1")] + let (R, s): (secp256k1::PublicKey, secp::MaybeScalar) = signature.unzip(); + # #[cfg(feature = "k256")] + let (R, s): (k256::PublicKey, k256::Scalar) = signature.unzip(); + # #[cfg(feature = "k256")] + let (R, s): (k256::AffinePoint, k256::Scalar) = signature.unzip(); +} +# +# Ok(()) +# } +``` +
+
+ +This crate exports [BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)-compatible compact Schnorr signature functionality as well. + +- [`verify_single`] - Single Schnorr signature verification. +- [`verify_batch`] - Efficient batched signature verification. +- [`sign_solo`] - Single-key message signing. + +## Serialization + +Binary and hex serialization with is implemented for the following types. + +- [`KeyAggContext`] +- [`SecNonce`] +- [`PubNonce`] +- [`AggNonce`] +- [`LiftedSignature`] +- [`CompactSignature`] + +This is accomplished through the [`BinaryEncoding`] trait. Aliases to the methods of [`BinaryEncoding`] are declared on the vanilla implementations of each type. In addition, these types all implement common standard library traits: + +- [`std::fmt::LowerHex`] +- [`std::fmt::UpperHex`] +- [`std::str::FromStr`] +- [`std::convert::TryFrom<&[u8]>`][std::convert::TryFrom] +- [`std::convert::TryFrom<[u8; N]>`][std::convert::TryFrom] (except [`KeyAggContext`]) +- [`std::convert::TryFrom<&[u8; N]>`][std::convert::TryFrom] (except [`KeyAggContext`]) + +They can also be infallibly converted to [`Vec`][Vec] using [`std::convert::From`], or to `[u8; N]` for fixed-length encodable types. + +If the `serde` feature is enabled, the above types implement [`serde::Serialize`] and [`serde::Deserialize`] for both binary and hex representations in constant time using the [`serdect`] crate. + +
+

Example

+ +```rust +# #[cfg(feature = "serde")] +# { +use musig2::{KeyAggContext, PubNonce, SecNonce}; + +#[derive(serde::Deserialize)] +struct CustomSigningSession { + key_agg_ctx: KeyAggContext, + pubnonces: Vec, + secnonce: SecNonce, + message: String, +} + +let json_data = "{ + \"key_agg_ctx\": \"034191a1714ff295b6bc1008aaab813ac5c47bb7d4e64065c0d488b35ead12e0ba\ + 000000020355d7de59c20355f9d8b14ccb60998983e9d73b38f64c9b9f2c4f868c\ + 0a7ac02f039582f6f17f99784bc6de6e5664ef5f69eb1bf0dc151d824b19481ab0717c0cd5\", + \"pubnonces\": [ + \"02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ + 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45\", + \"020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ + 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d\" + ], + \"secnonce\": \"B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB64\ + 95B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2\", + \"message\": \"attack at dawn\" +}"; + +let session: CustomSigningSession = serde_json::from_str(json_data).unwrap(); + +use musig2::BinaryEncoding; + +let key_agg_bytes: Vec = session.key_agg_ctx.to_bytes(); +let first_pubnonce_bytes: [u8; 66] = session.pubnonces[0].to_bytes(); +let secnonce_bytes = <[u8; 64]>::from(session.secnonce); + +let decoded_key_agg_ctx = KeyAggContext::from_bytes(&key_agg_bytes).unwrap(); +let decoded_pubnonce = PubNonce::try_from(&first_pubnonce_bytes).unwrap(); +let decoded_secnonce = SecNonce::try_from(secnonce_bytes).unwrap(); +# } +``` +
+
+ +# Security + +## Nonce Reuse + +The easiest pitfall for downstream instantiations of the MuSig2 protocol is accidental nonce reuse. If you ever reuse a [`SecNonce`] for two different signing sessions, [a co-signer can trick you into exposing your private key](https://medium.com/blockstream/musig-dn-schnorr-multisignatures-with-verifiably-deterministic-nonces-27424b5df9d6#e3b6). + +
+

But how?

+ +The malicious co-signer opens two signing sessions on the same message, and provides different nonces to the victim in both sessions. Even if the victim reuses _their_ secret nonce, a different nonce from a co-signer will result in different _aggregated_ nonces `R` and `R'` for both signing sessions. See [the Nonce Generation Algorithm in BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-nonce-aggregation) for more details on why this is. + +The challenge hash `e` is computed as `e = H(R, Q, m)` (where `Q` is the aggregated pubkey and `m` is the message). Since the aggregated nonce `R'` of the second session is different, this results in a new challenge hash `e' = H(R', Q, m)` for the second signing session. + +The victim's partial signatures `s` and `s'` for both signing sessions would be computed as: + +```notrust +s = k + e * a * d +s' = k + e' * a * d +``` + +...Where `d` is their secret key, `a` is a publicly known key-coefficient, and `k` is their secret nonce. + +Given both `s` and `s'` from the victim, the attacker can then solve for and compute the victim's private key `d`. + +```notrust +k = s - e * a * d +s' = k + e' * a * d +s' = s - e * a * d + e' * a * d +s' = s - a * d * (e + e') +a * d * (e + e') = s - s' +d = (s - s') / a * (e + e') +``` +
+
+ + +The [State-Machine API](#state-machine-api) is designed to avoid this possibility by computing and storing the [`SecNonce`] inside the [`FirstRound`] struct, and never exposing it directly to the downstream consumer. + +When using the `FirstRound` API, we recommend enabling the `rand` feature on this crate, and passing [`&mut rand::rngs::OsRng`][rand::rngs::OsRng] or [`&mut rand::thread_rng()`][rand::thread_rng] as the `nonce_seed` argument to [`FirstRound::new`]. This reduces the risk of accidental nonce reuse significantly. + +
+

Example

+ +```rust +# #[cfg(feature = "secp256k1")] +# use secp256k1::{SecretKey, PublicKey}; +# #[cfg(not(feature = "secp256k1"))] +# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; +# use musig2::KeyAggContext; +# +# /// Same pubkeys as in previous example +# let key_agg_ctx = +# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ +# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ +# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" +# .parse::() +# .unwrap(); +# +# let signer_index = 2; +# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" +# .parse() +# .unwrap(); +# +# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); +# +# // The group wants to sign something! +# let message = "hello interwebz!"; +use musig2::{FirstRound, SecNonceSpices}; + +# #[cfg(feature = "rand")] +let mut first_round = FirstRound::new( + key_agg_ctx, + &mut rand::rngs::OsRng, + signer_index, + SecNonceSpices::new() + .with_seckey(seckey) + .with_message(&message), +) +.unwrap(); +``` + +
+ +If you decide to use the Functional API instead for any reason, **you must ensure your code is adequately protected against accidental nonce reuse.** + +## Constant Time Operations + +All sensitive operations in this library endeavor to act in constant-time, independent of secret input. We mostly depend on the upstream [`k256`] and [`secp256k1`] crates for this functionality though, and no independent testing has confirmed this yet. diff --git a/crates/musig2/doc/adaptor_signatures.md b/crates/musig2/doc/adaptor_signatures.md new file mode 100644 index 00000000..d82213e8 --- /dev/null +++ b/crates/musig2/doc/adaptor_signatures.md @@ -0,0 +1,246 @@ +This module exports [adaptor signature](https://bitcoinops.org/en/topics/adaptor-signatures/) implementations of BIP340 signing and MuSig signing. + +Adaptor signatures allow signers to create Schnorr signatures which can be verified, but do not pass BIP340 verification logic unless a specific secret scalar is added to the signature. + +[Further reading](https://conduition.io/scriptless/adaptorsigs/). + +## MuSig Example + +Here we demonstrate a group of MuSig2 signers adaptor-signing the same message. The final signature which the group constructs is an [`AdaptorSignature`], which they cannot use until it has been decrypted (AKA 'adapted') by the correct adaptor secret (a scalar). + +```rust +use secp::{MaybeScalar, Point, Scalar}; +use musig2::{AdaptorSignature, KeyAggContext, PartialSignature, PubNonce}; + +let seckeys = [ + Scalar::from_slice(&[0x11; 32]).unwrap(), + Scalar::from_slice(&[0x22; 32]).unwrap(), + Scalar::from_slice(&[0x33; 32]).unwrap(), +]; + +let pubkeys = [ + seckeys[0].base_point_mul(), + seckeys[1].base_point_mul(), + seckeys[2].base_point_mul(), +]; + +let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); +let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + +let message = "danger, will robinson!"; + +let adaptor_secret = Scalar::random(&mut rand::thread_rng()); +let adaptor_point = adaptor_secret.base_point_mul(); + +// Using the functional API. +{ + use musig2::{AggNonce, SecNonce}; + + let secnonces = [ + SecNonce::build([0x11; 32]).build(), + SecNonce::build([0x22; 32]).build(), + SecNonce::build([0x33; 32]).build(), + ]; + + let pubnonces = [ + secnonces[0].public_nonce(), + secnonces[1].public_nonce(), + secnonces[2].public_nonce(), + ]; + + let aggnonce = AggNonce::sum(&pubnonces); + + let partial_signatures: Vec = seckeys + .into_iter() + .zip(secnonces) + .map(|(seckey, secnonce)| { + musig2::adaptor::sign_partial( + &key_agg_ctx, + seckey, + secnonce, + &aggnonce, + adaptor_point, + &message, + ) + }) + .collect::, _>>() + .expect("failed to create partial adaptor signatures"); + + let adaptor_signature: AdaptorSignature = musig2::adaptor::aggregate_partial_signatures( + &key_agg_ctx, + &aggnonce, + adaptor_point, + partial_signatures.iter().copied(), + &message, + ) + .expect("failed to aggregate partial adaptor signatures"); + + // Verify the adaptor signature is valid for the given adaptor point and pubkey. + musig2::adaptor::verify_single( + aggregated_pubkey, + &adaptor_signature, + &message, + adaptor_point, + ) + .expect("invalid aggregated adaptor signature"); + + // Decrypt the signature with the adaptor secret. + let valid_signature = adaptor_signature.adapt(adaptor_secret).unwrap(); + + musig2::verify_single( + aggregated_pubkey, + valid_signature, + &message, + ) + .expect("invalid decrypted adaptor signature"); + + // The decrypted signature and the adaptor signature allow an + // observer to deduce the adaptor secret. + let revealed: MaybeScalar = adaptor_signature + .reveal_secret(&valid_signature) + .expect("should compute adaptor secret from decrypted signature"); + + assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); +} + +// Using the state-machine API +{ + use musig2::{FirstRound, SecNonceSpices, SecondRound}; + + let spiced = |i| SecNonceSpices::new() + .with_seckey(seckeys[i]) + .with_message(&message); + + let mut first_rounds = vec![ + FirstRound::new(key_agg_ctx.clone(), [0x11; 32], 0, spiced(0)).unwrap(), + FirstRound::new(key_agg_ctx.clone(), [0x22; 32], 1, spiced(1)).unwrap(), + FirstRound::new(key_agg_ctx.clone(), [0x33; 32], 2, spiced(2)).unwrap(), + ]; + + let public_nonces = [ + first_rounds[0].our_public_nonce(), + first_rounds[1].our_public_nonce(), + first_rounds[2].our_public_nonce(), + ]; + + for round in first_rounds.iter_mut() { + round.receive_nonce(0, public_nonces[0].clone()).unwrap(); + round.receive_nonce(1, public_nonces[1].clone()).unwrap(); + round.receive_nonce(2, public_nonces[2].clone()).unwrap(); + } + + // The `finalize_adaptor` method must be used instead of `finalize`, on + // both first and second rounds. + let mut second_rounds: Vec> = first_rounds + .into_iter() + .enumerate() + .map(|(i, round)| round.finalize_adaptor(seckeys[i], adaptor_point, message).unwrap()) + .collect(); + + let partial_sigs: [PartialSignature; 3] = [ + second_rounds[0].our_signature(), + second_rounds[1].our_signature(), + second_rounds[2].our_signature(), + ]; + + for round in second_rounds.iter_mut() { + round.receive_signature(0, partial_sigs[0]).unwrap(); + round.receive_signature(1, partial_sigs[1]).unwrap(); + round.receive_signature(2, partial_sigs[2]).unwrap(); + } + + for second_round in second_rounds.into_iter() { + let adaptor_signature = second_round.finalize_adaptor::().unwrap(); + + // Verify the adaptor signature is valid for the given adaptor point and pubkey. + musig2::adaptor::verify_single( + aggregated_pubkey, + &adaptor_signature, + &message, + adaptor_point, + ) + .expect("invalid aggregated adaptor signature"); + + // Decrypt the signature with the adaptor secret. + let valid_signature = adaptor_signature.adapt(adaptor_secret).unwrap(); + musig2::verify_single( + aggregated_pubkey, + valid_signature, + &message, + ) + .expect("invalid decrypted adaptor signature"); + + // The decrypted signature and the adaptor signature allow an + // observer to deduce the adaptor secret. + let revealed: MaybeScalar = adaptor_signature + .reveal_secret(&valid_signature) + .expect("should compute adaptor secret from decrypted signature"); + + assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); + } +} +``` + +## Single Signer Example + +We also export single-signer adaptor signing logic. + +```rust +use secp::{MaybeScalar, Scalar}; + +let seckey = Scalar::random(&mut rand::rngs::OsRng); +let message = "hello world!"; + +// Create an adaptor signature, encrypted under a specific adaptor point. +let adaptor_secret = Scalar::random(&mut rand::rngs::OsRng); +let adaptor_point = adaptor_secret.base_point_mul(); +let aux_rand = [0xAA; 32]; // Should use an actual RNG. +let adaptor_signature = + musig2::adaptor::sign_solo(seckey, message, aux_rand, adaptor_point); + +// Verify the adaptor signature is valid for the given adaptor point and pubkey. +let pubkey = seckey.base_point_mul(); +musig2::adaptor::verify_single(pubkey, &adaptor_signature, message, adaptor_point) + .expect("valid adaptor signature should verify"); + +// Decrypt the signature with the adaptor secret. +let valid_sig: musig2::LiftedSignature = adaptor_signature + .adapt(adaptor_secret) + .expect("invalid adaptor secret"); + +musig2::verify_single(pubkey, valid_sig, message) + .expect("decrypted adaptor signature is valid"); + +// The decrypted signature and the adaptor signature allow an +// observer to deduce the adaptor secret. +let revealed: MaybeScalar = adaptor_signature.reveal_secret(&valid_sig) + .expect("decrypted sig should reveal adaptor secret"); +assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); +``` + +## Encrypting Signatures + +The above examples create signatures by committing to an adaptor point as part of the signing process. This is the way most adaptor signatures are created. + +However, you can also encrypt existing signatures (including existing adaptor signatures) by tweaking them with an adaptor secret. This requires knowing the adaptor secret though, so you can't do this if you only know the public adaptor point. + +```rust +let signature = musig2::LiftedSignature::from_hex( + "e565f19755422162cf7dc69ed8a4f4a27d81363d024a3de355644003da33ed2f\ + 0cdd95945c6d28841192867842c104391b9cc31f25706ee302a96204a1d43eb7" +) +.unwrap(); + +let adaptor_secret_1 = secp::Scalar::from_slice(&[0x55; 32]).unwrap(); +let mut adaptor_signature: musig2::AdaptorSignature = signature.encrypt(adaptor_secret_1); + +// We can re-encrypt the same adaptor signature twice, so that it is locked behind +// two different points. Both secrets must be learned to compute the valid signature. +let adaptor_secret_2 = secp::Scalar::from_slice(&[0x66; 32]).unwrap(); +adaptor_signature = adaptor_signature.encrypt(adaptor_secret_2); + +let decrypted: [u8; 64] = adaptor_signature + .adapt(adaptor_secret_1 + adaptor_secret_2) + .expect("valid decrypted adaptor signature"); +assert_eq!(decrypted, signature.serialize()); +``` diff --git a/crates/musig2/reference.py b/crates/musig2/reference.py new file mode 100644 index 00000000..b2d0436b --- /dev/null +++ b/crates/musig2/reference.py @@ -0,0 +1,882 @@ +# Source: https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py +# +# BIP327 reference implementation +# +# WARNING: This implementation is for demonstration purposes only and _not_ to +# be used in production environments. The code is vulnerable to timing attacks, +# for example. + +from typing import Any, List, Optional, Tuple, NewType, NamedTuple +import hashlib +import secrets +import time + +# +# The following helper functions were copied from the BIP-340 reference implementation: +# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py +# + +p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + +# Points are tuples of X and Y coordinates and the point at infinity is +# represented by the None keyword. +G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) + +Point = Tuple[int, int] + +# This implementation can be sped up by storing the midstate after hashing +# tag_hash instead of rehashing it all the time. +def tagged_hash(tag: str, msg: bytes) -> bytes: + tag_hash = hashlib.sha256(tag.encode()).digest() + return hashlib.sha256(tag_hash + tag_hash + msg).digest() + +def is_infinite(P: Optional[Point]) -> bool: + return P is None + +def x(P: Point) -> int: + assert not is_infinite(P) + return P[0] + +def y(P: Point) -> int: + assert not is_infinite(P) + return P[1] + +def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: + if P1 is None: + return P2 + if P2 is None: + return P1 + if (x(P1) == x(P2)) and (y(P1) != y(P2)): + return None + if P1 == P2: + lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p + else: + lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p + x3 = (lam * lam - x(P1) - x(P2)) % p + return (x3, (lam * (x(P1) - x3) - y(P1)) % p) + +def point_mul(P: Optional[Point], n: int) -> Optional[Point]: + R = None + for i in range(256): + if (n >> i) & 1: + R = point_add(R, P) + P = point_add(P, P) + return R + +def bytes_from_int(x: int) -> bytes: + return x.to_bytes(32, byteorder="big") + +def lift_x(b: bytes) -> Optional[Point]: + x = int_from_bytes(b) + if x >= p: + return None + y_sq = (pow(x, 3, p) + 7) % p + y = pow(y_sq, (p + 1) // 4, p) + if pow(y, 2, p) != y_sq: + return None + return (x, y if y & 1 == 0 else p-y) + +def int_from_bytes(b: bytes) -> int: + return int.from_bytes(b, byteorder="big") + +def has_even_y(P: Point) -> bool: + assert not is_infinite(P) + return y(P) % 2 == 0 + +def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: + if len(msg) != 32: + raise ValueError('The message must be a 32-byte array.') + if len(pubkey) != 32: + raise ValueError('The public key must be a 32-byte array.') + if len(sig) != 64: + raise ValueError('The signature must be a 64-byte array.') + P = lift_x(pubkey) + r = int_from_bytes(sig[0:32]) + s = int_from_bytes(sig[32:64]) + if (P is None) or (r >= p) or (s >= n): + return False + e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n + R = point_add(point_mul(G, s), point_mul(P, n - e)) + if (R is None) or (not has_even_y(R)) or (x(R) != r): + return False + return True + +# +# End of helper functions copied from BIP-340 reference implementation. +# + +PlainPk = NewType('PlainPk', bytes) +XonlyPk = NewType('XonlyPk', bytes) + +# There are two types of exceptions that can be raised by this implementation: +# - ValueError for indicating that an input doesn't conform to some function +# precondition (e.g. an input array is the wrong length, a serialized +# representation doesn't have the correct format). +# - InvalidContributionError for indicating that a signer (or the +# aggregator) is misbehaving in the protocol. +# +# Assertions are used to (1) satisfy the type-checking system, and (2) check for +# inconvenient events that can't happen except with negligible probability (e.g. +# output of a hash function is 0) and can't be manually triggered by any +# signer. + +# This exception is raised if a party (signer or nonce aggregator) sends invalid +# values. Actual implementations should not crash when receiving invalid +# contributions. Instead, they should hold the offending party accountable. +class InvalidContributionError(Exception): + def __init__(self, signer, contrib): + self.signer = signer + # contrib is one of "pubkey", "pubnonce", "aggnonce", or "psig". + self.contrib = contrib + +infinity = None + +def xbytes(P: Point) -> bytes: + return bytes_from_int(x(P)) + +def cbytes(P: Point) -> bytes: + a = b'\x02' if has_even_y(P) else b'\x03' + return a + xbytes(P) + +def cbytes_ext(P: Optional[Point]) -> bytes: + if is_infinite(P): + return (0).to_bytes(33, byteorder='big') + assert P is not None + return cbytes(P) + +def point_negate(P: Optional[Point]) -> Optional[Point]: + if P is None: + return P + return (x(P), p - y(P)) + +def cpoint(x: bytes) -> Point: + if len(x) != 33: + raise ValueError('x is not a valid compressed point.') + P = lift_x(x[1:33]) + if P is None: + raise ValueError('x is not a valid compressed point.') + if x[0] == 2: + return P + elif x[0] == 3: + P = point_negate(P) + assert P is not None + return P + else: + raise ValueError('x is not a valid compressed point.') + +def cpoint_ext(x: bytes) -> Optional[Point]: + if x == (0).to_bytes(33, 'big'): + return None + else: + return cpoint(x) + +# Return the plain public key corresponding to a given secret key +def individual_pk(seckey: bytes) -> PlainPk: + d0 = int_from_bytes(seckey) + if not (1 <= d0 <= n - 1): + raise ValueError('The secret key must be an integer in the range 1..n-1.') + P = point_mul(G, d0) + assert P is not None + return PlainPk(cbytes(P)) + +def key_sort(pubkeys: List[PlainPk]) -> List[PlainPk]: + pubkeys.sort() + return pubkeys + +KeyAggContext = NamedTuple('KeyAggContext', [('Q', Point), + ('gacc', int), + ('tacc', int)]) + +def get_xonly_pk(keyagg_ctx: KeyAggContext) -> XonlyPk: + Q, _, _ = keyagg_ctx + return XonlyPk(xbytes(Q)) + +def key_agg(pubkeys: List[PlainPk]) -> KeyAggContext: + pk2 = get_second_key(pubkeys) + u = len(pubkeys) + Q = infinity + for i in range(u): + try: + P_i = cpoint(pubkeys[i]) + except ValueError: + raise InvalidContributionError(i, "pubkey") + a_i = key_agg_coeff_internal(pubkeys, pubkeys[i], pk2) + Q = point_add(Q, point_mul(P_i, a_i)) + # Q is not the point at infinity except with negligible probability. + assert(Q is not None) + gacc = 1 + tacc = 0 + return KeyAggContext(Q, gacc, tacc) + +def hash_keys(pubkeys: List[PlainPk]) -> bytes: + return tagged_hash('KeyAgg list', b''.join(pubkeys)) + +def get_second_key(pubkeys: List[PlainPk]) -> PlainPk: + u = len(pubkeys) + for j in range(1, u): + if pubkeys[j] != pubkeys[0]: + return pubkeys[j] + return PlainPk(b'\x00'*33) + +def key_agg_coeff(pubkeys: List[PlainPk], pk_: PlainPk) -> int: + pk2 = get_second_key(pubkeys) + return key_agg_coeff_internal(pubkeys, pk_, pk2) + +def key_agg_coeff_internal(pubkeys: List[PlainPk], pk_: PlainPk, pk2: PlainPk) -> int: + L = hash_keys(pubkeys) + if pk_ == pk2: + return 1 + return int_from_bytes(tagged_hash('KeyAgg coefficient', L + pk_)) % n + +def apply_tweak(keyagg_ctx: KeyAggContext, tweak: bytes, is_xonly: bool) -> KeyAggContext: + if len(tweak) != 32: + raise ValueError('The tweak must be a 32-byte array.') + Q, gacc, tacc = keyagg_ctx + if is_xonly and not has_even_y(Q): + g = n - 1 + else: + g = 1 + t = int_from_bytes(tweak) + if t >= n: + raise ValueError('The tweak must be less than n.') + Q_ = point_add(point_mul(Q, g), point_mul(G, t)) + if Q_ is None: + raise ValueError('The result of tweaking cannot be infinity.') + gacc_ = g * gacc % n + tacc_ = (t + g * tacc) % n + return KeyAggContext(Q_, gacc_, tacc_) + +def bytes_xor(a: bytes, b: bytes) -> bytes: + return bytes(x ^ y for x, y in zip(a, b)) + +def nonce_hash(rand: bytes, pk: PlainPk, aggpk: XonlyPk, i: int, msg_prefixed: bytes, extra_in: bytes) -> int: + buf = b'' + buf += rand + buf += len(pk).to_bytes(1, 'big') + buf += pk + buf += len(aggpk).to_bytes(1, 'big') + buf += aggpk + buf += msg_prefixed + buf += len(extra_in).to_bytes(4, 'big') + buf += extra_in + buf += i.to_bytes(1, 'big') + return int_from_bytes(tagged_hash('MuSig/nonce', buf)) + +def nonce_gen_internal(rand_: bytes, sk: Optional[bytes], pk: PlainPk, aggpk: Optional[XonlyPk], msg: Optional[bytes], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: + if sk is not None: + rand = bytes_xor(sk, tagged_hash('MuSig/aux', rand_)) + else: + rand = rand_ + if aggpk is None: + aggpk = XonlyPk(b'') + if msg is None: + msg_prefixed = b'\x00' + else: + msg_prefixed = b'\x01' + msg_prefixed += len(msg).to_bytes(8, 'big') + msg_prefixed += msg + if extra_in is None: + extra_in = b'' + k_1 = nonce_hash(rand, pk, aggpk, 0, msg_prefixed, extra_in) % n + k_2 = nonce_hash(rand, pk, aggpk, 1, msg_prefixed, extra_in) % n + # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. + assert k_1 != 0 + assert k_2 != 0 + R_s1 = point_mul(G, k_1) + R_s2 = point_mul(G, k_2) + assert R_s1 is not None + assert R_s2 is not None + pubnonce = cbytes(R_s1) + cbytes(R_s2) + secnonce = bytearray(bytes_from_int(k_1) + bytes_from_int(k_2) + pk) + return secnonce, pubnonce + +def nonce_gen(sk: Optional[bytes], pk: PlainPk, aggpk: Optional[XonlyPk], msg: Optional[bytes], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: + if sk is not None and len(sk) != 32: + raise ValueError('The optional byte array sk must have length 32.') + if aggpk is not None and len(aggpk) != 32: + raise ValueError('The optional byte array aggpk must have length 32.') + rand_ = secrets.token_bytes(32) + return nonce_gen_internal(rand_, sk, pk, aggpk, msg, extra_in) + +def nonce_agg(pubnonces: List[bytes]) -> bytes: + u = len(pubnonces) + aggnonce = b'' + for j in (1, 2): + R_j = infinity + for i in range(u): + try: + R_ij = cpoint(pubnonces[i][(j-1)*33:j*33]) + except ValueError: + raise InvalidContributionError(i, "pubnonce") + R_j = point_add(R_j, R_ij) + aggnonce += cbytes_ext(R_j) + return aggnonce + +SessionContext = NamedTuple('SessionContext', [('aggnonce', bytes), + ('pubkeys', List[PlainPk]), + ('tweaks', List[bytes]), + ('is_xonly', List[bool]), + ('msg', bytes)]) + +def key_agg_and_tweak(pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool]): + if len(tweaks) != len(is_xonly): + raise ValueError('The `tweaks` and `is_xonly` arrays must have the same length.') + keyagg_ctx = key_agg(pubkeys) + v = len(tweaks) + for i in range(v): + keyagg_ctx = apply_tweak(keyagg_ctx, tweaks[i], is_xonly[i]) + return keyagg_ctx + +def get_session_values(session_ctx: SessionContext) -> Tuple[Point, int, int, int, Point, int]: + (aggnonce, pubkeys, tweaks, is_xonly, msg) = session_ctx + Q, gacc, tacc = key_agg_and_tweak(pubkeys, tweaks, is_xonly) + b = int_from_bytes(tagged_hash('MuSig/noncecoef', aggnonce + xbytes(Q) + msg)) % n + try: + R_1 = cpoint_ext(aggnonce[0:33]) + R_2 = cpoint_ext(aggnonce[33:66]) + except ValueError: + # Nonce aggregator sent invalid nonces + raise InvalidContributionError(None, "aggnonce") + R_ = point_add(R_1, point_mul(R_2, b)) + R = R_ if not is_infinite(R_) else G + assert R is not None + e = int_from_bytes(tagged_hash('BIP0340/challenge', xbytes(R) + xbytes(Q) + msg)) % n + return (Q, gacc, tacc, b, R, e) + +def get_session_key_agg_coeff(session_ctx: SessionContext, P: Point) -> int: + (_, pubkeys, _, _, _) = session_ctx + pk = PlainPk(cbytes(P)) + if pk not in pubkeys: + raise ValueError('The signer\'s pubkey must be included in the list of pubkeys.') + return key_agg_coeff(pubkeys, pk) + +def sign(secnonce: bytearray, sk: bytes, session_ctx: SessionContext) -> bytes: + (Q, gacc, _, b, R, e) = get_session_values(session_ctx) + k_1_ = int_from_bytes(secnonce[0:32]) + k_2_ = int_from_bytes(secnonce[32:64]) + # Overwrite the secnonce argument with zeros such that subsequent calls of + # sign with the same secnonce raise a ValueError. + secnonce[:64] = bytearray(b'\x00'*64) + if not 0 < k_1_ < n: + raise ValueError('first secnonce value is out of range.') + if not 0 < k_2_ < n: + raise ValueError('second secnonce value is out of range.') + k_1 = k_1_ if has_even_y(R) else n - k_1_ + k_2 = k_2_ if has_even_y(R) else n - k_2_ + d_ = int_from_bytes(sk) + if not 0 < d_ < n: + raise ValueError('secret key value is out of range.') + P = point_mul(G, d_) + assert P is not None + pk = cbytes(P) + if not pk == secnonce[64:97]: + raise ValueError('Public key does not match nonce_gen argument') + a = get_session_key_agg_coeff(session_ctx, P) + g = 1 if has_even_y(Q) else n - 1 + d = g * gacc * d_ % n + s = (k_1 + b * k_2 + e * a * d) % n + psig = bytes_from_int(s) + R_s1 = point_mul(G, k_1_) + R_s2 = point_mul(G, k_2_) + assert R_s1 is not None + assert R_s2 is not None + pubnonce = cbytes(R_s1) + cbytes(R_s2) + # Optional correctness check. The result of signing should pass signature verification. + assert partial_sig_verify_internal(psig, pubnonce, pk, session_ctx) + return psig + +def det_nonce_hash(sk_: bytes, aggothernonce: bytes, aggpk: bytes, msg: bytes, i: int) -> int: + buf = b'' + buf += sk_ + buf += aggothernonce + buf += aggpk + buf += len(msg).to_bytes(8, 'big') + buf += msg + buf += i.to_bytes(1, 'big') + return int_from_bytes(tagged_hash('MuSig/deterministic/nonce', buf)) + +def deterministic_sign(sk: bytes, aggothernonce: bytes, pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool], msg: bytes, rand: Optional[bytes]) -> Tuple[bytes, bytes]: + if rand is not None: + sk_ = bytes_xor(sk, tagged_hash('MuSig/aux', rand)) + else: + sk_ = sk + aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) + + k_1 = det_nonce_hash(sk_, aggothernonce, aggpk, msg, 0) % n + k_2 = det_nonce_hash(sk_, aggothernonce, aggpk, msg, 1) % n + # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. + assert k_1 != 0 + assert k_2 != 0 + + R_s1 = point_mul(G, k_1) + R_s2 = point_mul(G, k_2) + assert R_s1 is not None + assert R_s2 is not None + pubnonce = cbytes(R_s1) + cbytes(R_s2) + secnonce = bytearray(bytes_from_int(k_1) + bytes_from_int(k_2) + individual_pk(sk)) + try: + aggnonce = nonce_agg([pubnonce, aggothernonce]) + except Exception: + raise InvalidContributionError(None, "aggothernonce") + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + psig = sign(secnonce, sk, session_ctx) + return (pubnonce, psig) + +def partial_sig_verify(psig: bytes, pubnonces: List[bytes], pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool], msg: bytes, i: int) -> bool: + if len(pubnonces) != len(pubkeys): + raise ValueError('The `pubnonces` and `pubkeys` arrays must have the same length.') + if len(tweaks) != len(is_xonly): + raise ValueError('The `tweaks` and `is_xonly` arrays must have the same length.') + aggnonce = nonce_agg(pubnonces) + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + return partial_sig_verify_internal(psig, pubnonces[i], pubkeys[i], session_ctx) + +def partial_sig_verify_internal(psig: bytes, pubnonce: bytes, pk: bytes, session_ctx: SessionContext) -> bool: + (Q, gacc, _, b, R, e) = get_session_values(session_ctx) + s = int_from_bytes(psig) + if s >= n: + return False + R_s1 = cpoint(pubnonce[0:33]) + R_s2 = cpoint(pubnonce[33:66]) + Re_s_ = point_add(R_s1, point_mul(R_s2, b)) + Re_s = Re_s_ if has_even_y(R) else point_negate(Re_s_) + P = cpoint(pk) + if P is None: + return False + a = get_session_key_agg_coeff(session_ctx, P) + g = 1 if has_even_y(Q) else n - 1 + g_ = g * gacc % n + return point_mul(G, s) == point_add(Re_s, point_mul(P, e * a * g_ % n)) + +def partial_sig_agg(psigs: List[bytes], session_ctx: SessionContext) -> bytes: + (Q, _, tacc, _, R, e) = get_session_values(session_ctx) + s = 0 + u = len(psigs) + for i in range(u): + s_i = int_from_bytes(psigs[i]) + if s_i >= n: + raise InvalidContributionError(i, "psig") + s = (s + s_i) % n + g = 1 if has_even_y(Q) else n - 1 + s = (s + e * g * tacc) % n + return xbytes(R) + bytes_from_int(s) +# +# The following code is only used for testing. +# + +import json +import os +import sys + +def fromhex_all(l): + return [bytes.fromhex(l_i) for l_i in l] + +# Check that calling `try_fn` raises a `exception`. If `exception` is raised, +# examine it with `except_fn`. +def assert_raises(exception, try_fn, except_fn): + raised = False + try: + try_fn() + except exception as e: + raised = True + assert(except_fn(e)) + except BaseException: + raise AssertionError("Wrong exception raised in a test.") + if not raised: + raise AssertionError("Exception was _not_ raised in a test where it was required.") + +def get_error_details(test_case): + error = test_case["error"] + if error["type"] == "invalid_contribution": + exception = InvalidContributionError + if "contrib" in error: + except_fn = lambda e: e.signer == error["signer"] and e.contrib == error["contrib"] + else: + except_fn = lambda e: e.signer == error["signer"] + elif error["type"] == "value": + exception = ValueError + except_fn = lambda e: str(e) == error["message"] + else: + raise RuntimeError(f"Invalid error type: {error['type']}") + return exception, except_fn + +def test_key_sort_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'key_sort_vectors.json')) as f: + test_data = json.load(f) + + X = fromhex_all(test_data["pubkeys"]) + X_sorted = fromhex_all(test_data["sorted_pubkeys"]) + + assert key_sort(X) == X_sorted + +def test_key_agg_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'key_agg_vectors.json')) as f: + test_data = json.load(f) + + X = fromhex_all(test_data["pubkeys"]) + T = fromhex_all(test_data["tweaks"]) + valid_test_cases = test_data["valid_test_cases"] + error_test_cases = test_data["error_test_cases"] + + for test_case in valid_test_cases: + pubkeys = [X[i] for i in test_case["key_indices"]] + expected = bytes.fromhex(test_case["expected"]) + + assert get_xonly_pk(key_agg(pubkeys)) == expected + + for i, test_case in enumerate(error_test_cases): + exception, except_fn = get_error_details(test_case) + + pubkeys = [X[i] for i in test_case["key_indices"]] + tweaks = [T[i] for i in test_case["tweak_indices"]] + is_xonly = test_case["is_xonly"] + + assert_raises(exception, lambda: key_agg_and_tweak(pubkeys, tweaks, is_xonly), except_fn) + +def test_nonce_gen_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'nonce_gen_vectors.json')) as f: + test_data = json.load(f) + + for test_case in test_data["test_cases"]: + def get_value(key) -> bytes: + return bytes.fromhex(test_case[key]) + + def get_value_maybe(key) -> Optional[bytes]: + if test_case[key] is not None: + return get_value(key) + else: + return None + + rand_ = get_value("rand_") + sk = get_value_maybe("sk") + pk = PlainPk(get_value("pk")) + aggpk = get_value_maybe("aggpk") + if aggpk is not None: + aggpk = XonlyPk(aggpk) + msg = get_value_maybe("msg") + extra_in = get_value_maybe("extra_in") + expected_secnonce = get_value("expected_secnonce") + expected_pubnonce = get_value("expected_pubnonce") + + assert nonce_gen_internal(rand_, sk, pk, aggpk, msg, extra_in) == (expected_secnonce, expected_pubnonce) + +def test_nonce_agg_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'nonce_agg_vectors.json')) as f: + test_data = json.load(f) + + pnonce = fromhex_all(test_data["pnonces"]) + valid_test_cases = test_data["valid_test_cases"] + error_test_cases = test_data["error_test_cases"] + + for test_case in valid_test_cases: + pubnonces = [pnonce[i] for i in test_case["pnonce_indices"]] + expected = bytes.fromhex(test_case["expected"]) + assert nonce_agg(pubnonces) == expected + + for i, test_case in enumerate(error_test_cases): + exception, except_fn = get_error_details(test_case) + pubnonces = [pnonce[i] for i in test_case["pnonce_indices"]] + assert_raises(exception, lambda: nonce_agg(pubnonces), except_fn) + +def test_sign_verify_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'sign_verify_vectors.json')) as f: + test_data = json.load(f) + + sk = bytes.fromhex(test_data["sk"]) + X = fromhex_all(test_data["pubkeys"]) + # The public key corresponding to sk is at index 0 + assert X[0] == individual_pk(sk) + + secnonces = fromhex_all(test_data["secnonces"]) + pnonce = fromhex_all(test_data["pnonces"]) + # The public nonce corresponding to secnonces[0] is at index 0 + k_1 = int_from_bytes(secnonces[0][0:32]) + k_2 = int_from_bytes(secnonces[0][32:64]) + R_s1 = point_mul(G, k_1) + R_s2 = point_mul(G, k_2) + assert R_s1 is not None and R_s2 is not None + assert pnonce[0] == cbytes(R_s1) + cbytes(R_s2) + + aggnonces = fromhex_all(test_data["aggnonces"]) + # The aggregate of the first three elements of pnonce is at index 0 + assert(aggnonces[0] == nonce_agg([pnonce[0], pnonce[1], pnonce[2]])) + + msgs = fromhex_all(test_data["msgs"]) + + valid_test_cases = test_data["valid_test_cases"] + sign_error_test_cases = test_data["sign_error_test_cases"] + verify_fail_test_cases = test_data["verify_fail_test_cases"] + verify_error_test_cases = test_data["verify_error_test_cases"] + + for test_case in valid_test_cases: + pubkeys = [X[i] for i in test_case["key_indices"]] + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + aggnonce = aggnonces[test_case["aggnonce_index"]] + # Make sure that pubnonces and aggnonce in the test vector are + # consistent + assert nonce_agg(pubnonces) == aggnonce + msg = msgs[test_case["msg_index"]] + signer_index = test_case["signer_index"] + expected = bytes.fromhex(test_case["expected"]) + + session_ctx = SessionContext(aggnonce, pubkeys, [], [], msg) + # WARNING: An actual implementation should _not_ copy the secnonce. + # Reusing the secnonce, as we do here for testing purposes, can leak the + # secret key. + secnonce_tmp = bytearray(secnonces[0]) + assert sign(secnonce_tmp, sk, session_ctx) == expected + assert partial_sig_verify(expected, pubnonces, pubkeys, [], [], msg, signer_index) + + for i, test_case in enumerate(sign_error_test_cases): + exception, except_fn = get_error_details(test_case) + + pubkeys = [X[i] for i in test_case["key_indices"]] + aggnonce = aggnonces[test_case["aggnonce_index"]] + msg = msgs[test_case["msg_index"]] + secnonce = bytearray(secnonces[test_case["secnonce_index"]]) + + session_ctx = SessionContext(aggnonce, pubkeys, [], [], msg) + assert_raises(exception, lambda: sign(secnonce, sk, session_ctx), except_fn) + + for test_case in verify_fail_test_cases: + sig = bytes.fromhex(test_case["sig"]) + pubkeys = [X[i] for i in test_case["key_indices"]] + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + msg = msgs[test_case["msg_index"]] + signer_index = test_case["signer_index"] + + assert not partial_sig_verify(sig, pubnonces, pubkeys, [], [], msg, signer_index) + + for i, test_case in enumerate(verify_error_test_cases): + exception, except_fn = get_error_details(test_case) + + sig = bytes.fromhex(test_case["sig"]) + pubkeys = [X[i] for i in test_case["key_indices"]] + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + msg = msgs[test_case["msg_index"]] + signer_index = test_case["signer_index"] + + assert_raises(exception, lambda: partial_sig_verify(sig, pubnonces, pubkeys, [], [], msg, signer_index), except_fn) + +def test_tweak_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'tweak_vectors.json')) as f: + test_data = json.load(f) + + sk = bytes.fromhex(test_data["sk"]) + X = fromhex_all(test_data["pubkeys"]) + # The public key corresponding to sk is at index 0 + assert X[0] == individual_pk(sk) + + secnonce = bytearray(bytes.fromhex(test_data["secnonce"])) + pnonce = fromhex_all(test_data["pnonces"]) + # The public nonce corresponding to secnonce is at index 0 + k_1 = int_from_bytes(secnonce[0:32]) + k_2 = int_from_bytes(secnonce[32:64]) + R_s1 = point_mul(G, k_1) + R_s2 = point_mul(G, k_2) + assert R_s1 is not None and R_s2 is not None + assert pnonce[0] == cbytes(R_s1) + cbytes(R_s2) + + aggnonce = bytes.fromhex(test_data["aggnonce"]) + # The aggnonce is the aggregate of the first three elements of pnonce + assert(aggnonce == nonce_agg([pnonce[0], pnonce[1], pnonce[2]])) + + tweak = fromhex_all(test_data["tweaks"]) + msg = bytes.fromhex(test_data["msg"]) + + valid_test_cases = test_data["valid_test_cases"] + error_test_cases = test_data["error_test_cases"] + + for test_case in valid_test_cases: + pubkeys = [X[i] for i in test_case["key_indices"]] + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + tweaks = [tweak[i] for i in test_case["tweak_indices"]] + is_xonly = test_case["is_xonly"] + signer_index = test_case["signer_index"] + expected = bytes.fromhex(test_case["expected"]) + + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + secnonce_tmp = bytearray(secnonce) + # WARNING: An actual implementation should _not_ copy the secnonce. + # Reusing the secnonce, as we do here for testing purposes, can leak the + # secret key. + assert sign(secnonce_tmp, sk, session_ctx) == expected + assert partial_sig_verify(expected, pubnonces, pubkeys, tweaks, is_xonly, msg, signer_index) + + for i, test_case in enumerate(error_test_cases): + exception, except_fn = get_error_details(test_case) + + pubkeys = [X[i] for i in test_case["key_indices"]] + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + tweaks = [tweak[i] for i in test_case["tweak_indices"]] + is_xonly = test_case["is_xonly"] + signer_index = test_case["signer_index"] + + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + assert_raises(exception, lambda: sign(secnonce, sk, session_ctx), except_fn) + +def test_det_sign_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'det_sign_vectors.json')) as f: + test_data = json.load(f) + + sk = bytes.fromhex(test_data["sk"]) + X = fromhex_all(test_data["pubkeys"]) + # The public key corresponding to sk is at index 0 + assert X[0] == individual_pk(sk) + + msgs = fromhex_all(test_data["msgs"]) + + valid_test_cases = test_data["valid_test_cases"] + error_test_cases = test_data["error_test_cases"] + + for test_case in valid_test_cases: + pubkeys = [X[i] for i in test_case["key_indices"]] + aggothernonce = bytes.fromhex(test_case["aggothernonce"]) + tweaks = fromhex_all(test_case["tweaks"]) + is_xonly = test_case["is_xonly"] + msg = msgs[test_case["msg_index"]] + signer_index = test_case["signer_index"] + rand = bytes.fromhex(test_case["rand"]) if test_case["rand"] is not None else None + expected = fromhex_all(test_case["expected"]) + + pubnonce, psig = deterministic_sign(sk, aggothernonce, pubkeys, tweaks, is_xonly, msg, rand) + assert pubnonce == expected[0] + assert psig == expected[1] + + pubnonces = [aggothernonce, pubnonce] + aggnonce = nonce_agg(pubnonces) + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + assert partial_sig_verify_internal(psig, pubnonce, pubkeys[signer_index], session_ctx) + + for i, test_case in enumerate(error_test_cases): + exception, except_fn = get_error_details(test_case) + + pubkeys = [X[i] for i in test_case["key_indices"]] + aggothernonce = bytes.fromhex(test_case["aggothernonce"]) + tweaks = fromhex_all(test_case["tweaks"]) + is_xonly = test_case["is_xonly"] + msg = msgs[test_case["msg_index"]] + signer_index = test_case["signer_index"] + rand = bytes.fromhex(test_case["rand"]) if test_case["rand"] is not None else None + + try_fn = lambda: deterministic_sign(sk, aggothernonce, pubkeys, tweaks, is_xonly, msg, rand) + assert_raises(exception, try_fn, except_fn) + +def test_sig_agg_vectors() -> None: + with open(os.path.join(sys.path[0], 'vectors', 'sig_agg_vectors.json')) as f: + test_data = json.load(f) + + X = fromhex_all(test_data["pubkeys"]) + + # These nonces are only required if the tested API takes the individual + # nonces and not the aggregate nonce. + pnonce = fromhex_all(test_data["pnonces"]) + + tweak = fromhex_all(test_data["tweaks"]) + psig = fromhex_all(test_data["psigs"]) + + msg = bytes.fromhex(test_data["msg"]) + + valid_test_cases = test_data["valid_test_cases"] + error_test_cases = test_data["error_test_cases"] + + for test_case in valid_test_cases: + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + aggnonce = bytes.fromhex(test_case["aggnonce"]) + assert aggnonce == nonce_agg(pubnonces) + + pubkeys = [X[i] for i in test_case["key_indices"]] + tweaks = [tweak[i] for i in test_case["tweak_indices"]] + is_xonly = test_case["is_xonly"] + psigs = [psig[i] for i in test_case["psig_indices"]] + expected = bytes.fromhex(test_case["expected"]) + + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + sig = partial_sig_agg(psigs, session_ctx) + assert sig == expected + aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) + assert schnorr_verify(msg, aggpk, sig) + + for i, test_case in enumerate(error_test_cases): + exception, except_fn = get_error_details(test_case) + + pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] + aggnonce = nonce_agg(pubnonces) + + pubkeys = [X[i] for i in test_case["key_indices"]] + tweaks = [tweak[i] for i in test_case["tweak_indices"]] + is_xonly = test_case["is_xonly"] + psigs = [psig[i] for i in test_case["psig_indices"]] + + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + assert_raises(exception, lambda: partial_sig_agg(psigs, session_ctx), except_fn) + +def test_sign_and_verify_random(iters: int) -> None: + for i in range(iters): + sk_1 = secrets.token_bytes(32) + sk_2 = secrets.token_bytes(32) + pk_1 = individual_pk(sk_1) + pk_2 = individual_pk(sk_2) + pubkeys = [pk_1, pk_2] + + # In this example, the message and aggregate pubkey are known + # before nonce generation, so they can be passed into the nonce + # generation function as a defense-in-depth measure to protect + # against nonce reuse. + # + # If these values are not known when nonce_gen is called, empty + # byte arrays can be passed in for the corresponding arguments + # instead. + msg = secrets.token_bytes(32) + v = secrets.randbelow(4) + tweaks = [secrets.token_bytes(32) for _ in range(v)] + is_xonly = [secrets.choice([False, True]) for _ in range(v)] + aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) + + # Use a non-repeating counter for extra_in + secnonce_1, pubnonce_1 = nonce_gen(sk_1, pk_1, aggpk, msg, i.to_bytes(4, 'big')) + + # On even iterations use regular signing algorithm for signer 2, + # otherwise use deterministic signing algorithm + if i % 2 == 0: + # Use a clock for extra_in + t = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + secnonce_2, pubnonce_2 = nonce_gen(sk_2, pk_2, aggpk, msg, t.to_bytes(8, 'big')) + else: + aggothernonce = nonce_agg([pubnonce_1]) + rand = secrets.token_bytes(32) + pubnonce_2, psig_2 = deterministic_sign(sk_2, aggothernonce, pubkeys, tweaks, is_xonly, msg, rand) + + pubnonces = [pubnonce_1, pubnonce_2] + aggnonce = nonce_agg(pubnonces) + + session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) + psig_1 = sign(secnonce_1, sk_1, session_ctx) + assert partial_sig_verify(psig_1, pubnonces, pubkeys, tweaks, is_xonly, msg, 0) + # An exception is thrown if secnonce_1 is accidentally reused + assert_raises(ValueError, lambda: sign(secnonce_1, sk_1, session_ctx), lambda e: True) + + # Wrong signer index + assert not partial_sig_verify(psig_1, pubnonces, pubkeys, tweaks, is_xonly, msg, 1) + + # Wrong message + assert not partial_sig_verify(psig_1, pubnonces, pubkeys, tweaks, is_xonly, secrets.token_bytes(32), 0) + + if i % 2 == 0: + psig_2 = sign(secnonce_2, sk_2, session_ctx) + assert partial_sig_verify(psig_2, pubnonces, pubkeys, tweaks, is_xonly, msg, 1) + + sig = partial_sig_agg([psig_1, psig_2], session_ctx) + assert schnorr_verify(msg, aggpk, sig) + +if __name__ == '__main__': + test_key_sort_vectors() + test_key_agg_vectors() + test_nonce_gen_vectors() + test_nonce_agg_vectors() + test_sign_verify_vectors() + test_tweak_vectors() + test_det_sign_vectors() + test_sig_agg_vectors() + test_sign_and_verify_random(6) diff --git a/crates/musig2/src/binary_encoding.rs b/crates/musig2/src/binary_encoding.rs new file mode 100644 index 00000000..47bb2bb0 --- /dev/null +++ b/crates/musig2/src/binary_encoding.rs @@ -0,0 +1,295 @@ +use crate::errors::DecodeError; + +/// Marks a type which can be serialized to and from a binary encoding of either +/// fixed or variable length. +pub trait BinaryEncoding: Sized { + /// The binary type which is returned by serialization. Should either + /// be `[u8; N]` or `Vec`. + type Serialized; + + /// Serialize this data structure to its binary representation. + fn to_bytes(&self) -> Self::Serialized; + + /// Deserialize this data structure from a binary representation. + fn from_bytes(bytes: &[u8]) -> Result>; +} + +/// Implements various binary encoding traits for both fixed or +/// variable-length encoded data structures. +/// +/// Use this macro by first implementing [`BinaryEncoding`] on a type, +/// and then invoking `impl_encoding_traits` on the type. +macro_rules! impl_encoding_traits { + // Fixed length encoding + ($typename:ty, $byte_len:expr $(, $max_byte_len:expr)?) => { + /// assert that $typename implements `BinaryEncoding` + const _: () = { + fn __( + x: $typename, + ) -> impl BinaryEncoding + { + x + } + }; + + impl std::fmt::LowerHex for $typename { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut buffer = [0; $byte_len * 2]; + let encoded = base16ct::lower::encode_str(&self.to_bytes(), &mut buffer).unwrap(); + f.write_str(encoded) + } + } + + impl std::fmt::UpperHex for $typename { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let mut buffer = [0; $byte_len * 2]; + let encoded = base16ct::upper::encode_str(&self.to_bytes(), &mut buffer).unwrap(); + f.write_str(encoded) + } + } + + impl std::str::FromStr for $typename { + type Err = DecodeError; + + /// Parses this type from a hex string, which can be either upper or + /// lower case. The binary format of the decoded hex data should + /// match that returned by [`to_bytes`][Self::to_bytes]. + /// + /// Same as [`Self::from_hex`]. + fn from_str(hex: &str) -> Result { + let mut buffer = [0; $byte_len]; + let bytes = base16ct::mixed::decode(hex, &mut buffer)?; + Self::from_bytes(bytes) + } + } + + impl TryFrom<&[u8]> for $typename { + type Error = DecodeError; + + /// Parse this type from a variable-length byte slice. + /// + /// Same as [`Self::from_bytes`][Self::from_bytes]. + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } + } + + impl TryFrom<[u8; $byte_len]> for $typename { + type Error = DecodeError; + + /// Parse this type from its fixed-length binary representation. + fn try_from(bytes: [u8; $byte_len]) -> Result { + Self::from_bytes(&bytes) + } + } + + impl TryFrom<&[u8; $byte_len]> for $typename { + type Error = DecodeError; + + /// Parse this type from its fixed-length binary representation. + /// + /// Same as [`Self::from_bytes`][Self::from_bytes]. + fn try_from(bytes: &[u8; $byte_len]) -> Result { + Self::from_bytes(bytes) + } + } + + $( + impl TryFrom<&[u8; $max_byte_len]> for $typename { + type Error = DecodeError; + + /// Parse this type from its maximum-length binary representation. + /// Throws away unused data. + /// + /// Same as [`Self::from_bytes`][Self::from_bytes]. + fn try_from(bytes: &[u8; $max_byte_len]) -> Result { + Self::from_bytes(bytes) + } + } + )? + + impl From<$typename> for [u8; $byte_len] { + /// Serialize this type to a fixed-length byte array. + fn from(value: $typename) -> Self { + value.to_bytes() + } + } + + impl From<$typename> for Vec { + /// Serialize this type to a heap-allocated byte vector. + fn from(value: $typename) -> Self { + Vec::from(value.to_bytes()) + } + } + + impl $typename { + /// Alias to [the `BinaryEncoding` trait implementation of `to_bytes`][Self::to_bytes]. + pub fn serialize(&self) -> [u8; $byte_len] { + ::to_bytes(self) + } + + /// Alias to [the `BinaryEncoding` trait implementation of `from_bytes`][Self::from_bytes]. + pub fn from_bytes(bytes: &[u8]) -> Result> { + ::from_bytes(bytes) + } + + /// Parses this type from a hex string, which can be either upper or + /// lower case. The binary format of the decoded hex data should + /// match that returned by [`to_bytes`][Self::to_bytes]. + /// + /// Same as [`Self::from_str`](#method.from_str). + pub fn from_hex(hex: &str) -> Result> { + hex.parse() + } + } + + #[cfg(any(test, feature = "serde"))] + impl serde::Serialize for $typename { + fn serialize(&self, serializer: S) -> Result { + let bytes = self.to_bytes(); + serdect::array::serialize_hex_lower_or_bin(&bytes, serializer) + } + } + + #[cfg(any(test, feature = "serde"))] + impl<'de> serde::Deserialize<'de> for $typename { + /// Deserializes this type from a byte array or a hex + /// string, depending on the human-readability of the data format. + fn deserialize>(deserializer: D) -> Result { + #[allow(unused_mut, unused_variables)] + let mut buffer = [0u8; $byte_len]; + + // Used for a type like SecNonce where we need to accept a longer encoding + // and throw away the unused bytes. + $(let mut buffer = [0u8; $max_byte_len];)? + + let bytes = serdect::slice::deserialize_hex_or_bin(&mut buffer, deserializer)?; + <$typename>::from_bytes(bytes).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Bytes(&bytes), + &concat!("a byte array representing ", stringify!($typename)), + ) + }) + } + } + }; + + // Variable-length encoding + ($typename:ty) => { + /// assert that $typename implements `BinaryEncoding` + const _: () = { + fn __( + x: $typename, + ) -> impl BinaryEncoding> { + x + } + }; + + impl std::fmt::LowerHex for $typename { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let bytes = self.to_bytes(); + let mut buffer = vec![0; bytes.len() * 2]; + let encoded = base16ct::lower::encode_str(&bytes, &mut buffer).unwrap(); + f.write_str(encoded) + } + } + + impl std::fmt::UpperHex for $typename { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let bytes = self.to_bytes(); + let mut buffer = vec![0; bytes.len() * 2]; + let encoded = base16ct::upper::encode_str(&bytes, &mut buffer).unwrap(); + f.write_str(encoded) + } + } + + impl std::str::FromStr for $typename { + type Err = DecodeError; + + /// Parses this type from a hex string, which can be either upper or + /// lower case. The binary format of the decoded hex data should + /// match that returned by [`to_bytes`][Self::to_bytes]. + /// + /// Same as [`Self::from_hex`]. + fn from_str(hex: &str) -> Result { + let bytes = base16ct::mixed::decode_vec(hex)?; + Self::from_bytes(&bytes) + } + } + + impl TryFrom<&[u8]> for $typename { + type Error = DecodeError; + + /// Parse this type from a variable-length byte slice. + /// + /// Same as [`Self::from_bytes`][Self::from_bytes]. + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } + } + + impl From<$typename> for Vec { + /// Serialize this type to a heap-allocated byte vector. + fn from(value: $typename) -> Self { + value.to_bytes() + } + } + + impl $typename { + /// Alias to [the `BinaryEncoding` trait implementation of `to_bytes`][Self::to_bytes]. + pub fn serialize(&self) -> Vec { + ::to_bytes(self) + } + + /// Alias to [the `BinaryEncoding` trait implementation of `from_bytes`][Self::from_bytes]. + pub fn from_bytes(bytes: &[u8]) -> Result> { + ::from_bytes(bytes) + } + + /// Parses this type from a hex string, which can be either upper or + /// lower case. The binary format of the decoded hex data should + /// match that returned by [`to_bytes`][Self::to_bytes]. + /// + /// Same as [`Self::from_str`](#method.from_str). + pub fn from_hex(hex: &str) -> Result> { + hex.parse() + } + } + + #[cfg(any(test, feature = "serde"))] + impl serde::Serialize for $typename { + fn serialize(&self, serializer: S) -> Result { + let bytes = self.to_bytes(); + serdect::slice::serialize_hex_lower_or_bin(&bytes, serializer) + } + } + + #[cfg(any(test, feature = "serde"))] + impl<'de> serde::Deserialize<'de> for $typename { + /// Deserializes this type from a byte vector or a hex + /// string, depending on the human-readability of the data format. + fn deserialize>(deserializer: D) -> Result { + let bytes = serdect::slice::deserialize_hex_or_bin_vec(deserializer)?; + <$typename>::from_bytes(&bytes).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Bytes(&bytes), + &concat!("a byte vector representing ", stringify!($typename)), + ) + }) + } + } + }; +} + +/// Implements the Display trait for a type by formatting it as a lower-case +/// hex string. +macro_rules! impl_hex_display { + ($typename:ident) => { + impl std::fmt::Display for $typename { + /// Formats this type as a lower-case hex string. + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{:x}", self) + } + } + }; +} diff --git a/crates/musig2/src/bip340.rs b/crates/musig2/src/bip340.rs new file mode 100644 index 00000000..23e31efa --- /dev/null +++ b/crates/musig2/src/bip340.rs @@ -0,0 +1,445 @@ +use crate::errors::VerifyError; +use crate::{ + compute_challenge_hash_tweak, tagged_hashes, xor_bytes, AdaptorSignature, CompactSignature, + LiftedSignature, NonceSeed, +}; + +use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; + +#[cfg(any(test, feature = "rand"))] +use rand::SeedableRng as _; + +use sha2::Digest as _; +use subtle::ConstantTimeEq as _; + +/// Create a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) +/// Schnorr adaptor signature on the given message with a single private key. +/// +/// The resulting signature is verifiably encrypted under the given `adaptor_point`, +/// such that it can only be considered valid under BIP340 if it is then +/// _adapted_ using the discrete log of `adaptor_point`. See +/// [`AdaptorSignature::adapt`] to decrypt it once you know the adaptor secret. +/// +/// You can also compute the adaptor secret from the final decrypted signature, +/// if you can find it. +/// +/// This is provided in case MuSig implementations may wish to make use of +/// signatures to non-interactively prove the origin of a message. For example, +/// if all messages between co-signers are signed, then peers can assign blame +/// to any dishonest signers by sharing a copy of their dishonest message, which +/// will bear their signature. +pub fn sign_solo_adaptor( + seckey: impl Into, + message: impl AsRef<[u8]>, + nonce_seed: impl Into, + adaptor_point: impl Into, +) -> AdaptorSignature { + let seckey: Scalar = seckey.into(); + let nonce_seed: NonceSeed = nonce_seed.into(); + + let pubkey = seckey.base_point_mul(); + let d = seckey.negate_if(pubkey.parity()); + + let h: [u8; 32] = tagged_hashes::BIP0340_AUX_TAG_HASHER + .clone() + .chain_update(nonce_seed.0) + .finalize() + .into(); + + let t = xor_bytes(&h, &d.serialize()); + + let rand: [u8; 32] = tagged_hashes::BIP0340_NONCE_TAG_HASHER + .clone() + .chain_update(t) + .chain_update(pubkey.serialize_xonly()) + .chain_update(message.as_ref()) + .finalize() + .into(); + + // BIP340 says to fail if we get a nonce reducing to zero, but this is so + // unlikely that the failure condition is not worth it. Default to 1 instead. + let prenonce = match MaybeScalar::reduce_from(&rand) { + MaybeScalar::Zero => Scalar::one(), + MaybeScalar::Valid(k) => k, + }; + + let R = prenonce * G; // encrypted nonce + let adapted_nonce = R + adaptor_point.into(); + + // If the adapted nonce is odd-parity, we must negate our nonce and + // later also negate the adaptor secret at decryption time. + let k = prenonce.negate_if(adapted_nonce.parity()); + + let nonce_x_bytes = adapted_nonce.serialize_xonly(); + let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &pubkey, message); + + let s = k + e * d; + + AdaptorSignature::new(R, s) +} + +/// Create a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) +/// Schnorr signature on the given message with a single private key. +/// +/// This is provided in case MuSig implementations may wish to make use of +/// signatures to non-interactively prove the origin of a message. For example, +/// if all messages between co-signers are signed, then peers can assign blame +/// to any dishonest signers by sharing a copy of their dishonest message, which +/// will bear their signature. +/// +/// This function is effectively the same as [`sign_solo_adaptor`] but passing +/// [`MaybePoint::Infinity`] as the adaptor point. +pub fn sign_solo( + seckey: impl Into, + message: impl AsRef<[u8]>, + nonce_seed: impl Into, +) -> T +where + T: From, +{ + sign_solo_adaptor(seckey, message, nonce_seed, MaybePoint::Infinity) + .adapt(MaybeScalar::Zero) + .map(T::from) + .expect("signing with empty adaptor should never result in an adaptor failure") +} + +/// Verifies a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) +/// Schnorr adaptor signature, which could be aggregated or from a single-signer. +/// +/// The signature will verify only if it is encrypted under the given adaptor point. +/// +/// The `signature` argument is parsed as a [`LiftedSignature`]. You may pass any +/// type which converts fallibly to a [`LiftedSignature`], including `&[u8]`, `[u8; 64]`, +/// [`CompactSignature`], and so on. +/// +/// Returns an error if the adaptor signature is invalid, which includes +/// if the signature has been decrypted and is a fully valid signature. +pub fn verify_single_adaptor( + pubkey: impl Into, + adaptor_signature: &AdaptorSignature, + message: impl AsRef<[u8]>, + adaptor_point: impl Into, +) -> Result<(), VerifyError> { + use VerifyError::BadSignature; + + let pubkey: Point = pubkey.into().to_even_y(); // lift_x(x(P)) + + let &AdaptorSignature { R, s } = adaptor_signature; + + let adapted_nonce = R + adaptor_point.into(); + let nonce_x_bytes = adapted_nonce.serialize_xonly(); + let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &pubkey, message); + + // If the adapted nonce is odd-parity, the signer should have negated their nonce + // when signing. + let effective_nonce = if adapted_nonce.has_even_y() { R } else { -R }; + + // sG = R + eD + if s * G != effective_nonce + e * pubkey { + return Err(BadSignature); + } + + Ok(()) +} + +/// Verifies a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) +/// Schnorr signature, which could be aggregated or from a single-signer. +/// +/// The `signature` argument is parsed as a [`CompactSignature`]. You may pass any +/// type which converts fallibly to a [`CompactSignature`], including `&[u8]`, `[u8; 64]`, +/// [`LiftedSignature`], and so on. +/// +/// Returns an error if the signature is invalid. +pub fn verify_single( + pubkey: impl Into, + signature: impl TryInto, + message: impl AsRef<[u8]>, +) -> Result<(), VerifyError> { + use VerifyError::BadSignature; + + let pubkey: Point = pubkey.into().to_even_y(); // lift_x(x(P)) + let CompactSignature { rx, s } = signature.try_into().map_err(|_| BadSignature)?; + let e: MaybeScalar = compute_challenge_hash_tweak(&rx, &pubkey, message); + + // Instead of the usual sG = R + eD schnorr equation, we swap things around + // slightly, thus avoiding the need to lift the x-only nonce. + // + // sG = R + eD + // R = sG - eD + let verification_point = (s * G - e * pubkey).not_inf().map_err(|_| BadSignature)?; + if verification_point.has_odd_y() { + return Err(BadSignature); + } + + let valid = verification_point.serialize_xonly().ct_eq(&rx); + if bool::from(valid) { + Ok(()) + } else { + Err(BadSignature) + } +} + +/// Represents a pre-processed entry in a batch of signatures to be verified. +/// This can encapsulate either a normal BIP340 signature, or an adaptor signature. +/// +/// To verify a large number of signatures efficiently, pass a slice of +/// [`BatchVerificationRow`] to [`verify_batch`]. +#[cfg(any(test, feature = "rand"))] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BatchVerificationRow { + pubkey: Point, + challenge: MaybeScalar, + R: MaybePoint, + s: MaybeScalar, +} + +#[cfg(any(test, feature = "rand"))] +impl BatchVerificationRow { + /// Construct a row in a batch verification table from a given BIP340 signature. + pub fn from_signature>( + pubkey: impl Into, + message: M, + signature: LiftedSignature, + ) -> Self { + let pubkey = pubkey.into(); + let challenge = + compute_challenge_hash_tweak(&signature.R.serialize_xonly(), &pubkey, message.as_ref()); + + BatchVerificationRow { + pubkey, + challenge, + R: MaybePoint::Valid(signature.R), + s: signature.s, + } + } + + /// Construct a row in a batch verification table from a given BIP340 adaptor signature. + pub fn from_adaptor_signature>( + pubkey: impl Into, + message: M, + adaptor_signature: AdaptorSignature, + adaptor_point: MaybePoint, + ) -> Self { + let pubkey = pubkey.into(); + let adapted_nonce = adaptor_signature.R + adaptor_point; + + // If the adapted nonce is odd-parity, the signer should have negated their nonce + // when signing. + let effective_nonce = if adapted_nonce.has_even_y() { + adaptor_signature.R + } else { + -adaptor_signature.R + }; + + let challenge = compute_challenge_hash_tweak( + &adapted_nonce.serialize_xonly(), + &pubkey, + message.as_ref(), + ); + + BatchVerificationRow { + pubkey, + challenge, + R: effective_nonce, + s: adaptor_signature.s, + } + } +} + +/// Runs [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) +/// batch verification on a collection of schnorr signatures. +/// +/// Batch verification checks a table of pubkeys, messages, and +/// signatures and returns an error if any signatures in the +/// collection are not valid for the corresponding `(pubkey, message)` +/// pair. +/// +/// Batch verification enables noteworthy speedups when verifying +/// large numbers of signatures, but does not give any indication +/// of _which_ signature(s) were invalid upon failure. Manual +/// investigation would be needed to narrow down which signature(s) +/// caused the verification to fail. +/// +/// This requires the `rand` library for access to a seedable CSPRNG. +/// The RNG is seeded with all the pubkeys, messages, and signatures +/// rather than being truly random. +#[cfg(any(test, feature = "rand"))] +pub fn verify_batch(rows: &[BatchVerificationRow]) -> Result<(), VerifyError> { + // Seed the CSPRNG + let mut rng = { + let mut seed_hash = tagged_hashes::BIP0340_BATCH_TAG_HASHER.clone(); + + // Challenges commit to the pubkey, nonce, and message. That's why + // we're not explicitly seeding the RNG with the pubkey, nonce, and message + // as suggested by BIP340. + for row in rows { + seed_hash.update(row.challenge.serialize()); + } + + for row in rows { + seed_hash.update(row.s.serialize()); + } + rand::rngs::StdRng::from_seed(seed_hash.finalize().into()) + }; + + let mut lhs = MaybeScalar::Zero; + let mut rhs_terms = Vec::::with_capacity(rows.len() * 2); + + for (i, row) in rows.iter().enumerate() { + let random = if i == 0 { + Scalar::one() + } else { + Scalar::random(&mut rng) + }; + + let pubkey = row.pubkey.to_even_y(); // lift_x on all pubkeys + + lhs += row.s * random; + rhs_terms.push(row.R * random); + rhs_terms.push((random * row.challenge) * pubkey); + } + + // (s1*a1 + s2*a2 + ... + sn*an)G ?= (a1*R1) + (a2*R2) + ... + (an*Rn) + + // (a1*e1*P1) + (a2*e2*P2) + ... + (an*en*Pn) + let rhs = MaybePoint::sum(rhs_terms); + if lhs * G == rhs { + Ok(()) + } else { + Err(VerifyError::BadSignature) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{testhex, BinaryEncoding, CompactSignature}; + use secp::Scalar; + + #[test] + fn test_bip340_signatures() { + const BIP340_TEST_VECTORS: &[u8] = include_bytes!("test_vectors/bip340_vectors.csv"); + + #[derive(serde::Deserialize)] + struct TestVectorRecord { + index: usize, + #[serde(rename = "secret key")] + seckey: Option, + #[serde(rename = "public key", deserialize_with = "testhex::deserialize")] + pubkey_x: [u8; 32], + #[serde(deserialize_with = "testhex::deserialize")] + aux_rand: Vec, + #[serde(deserialize_with = "testhex::deserialize")] + message: Vec, + signature: String, + #[serde(rename = "verification result")] + verification_result: String, + comment: String, + } + + let mut csv_reader = csv::Reader::from_reader(BIP340_TEST_VECTORS); + + let mut valid_sigs_batch = Vec::::new(); + + for result in csv_reader.deserialize() { + let record: TestVectorRecord = result.expect("failed to parse BIP340 test vector"); + + let pubkey = match Point::lift_x(&record.pubkey_x) { + Ok(p) => p, + Err(_) => { + if record.verification_result == "TRUE" { + panic!( + "expected verification to succeed on invalid public key {}", + base16ct::lower::encode_string(&record.pubkey_x) + ); + } + continue; // not a test case we have to worry about. + } + }; + + let test_vec_signature: [u8; 64] = base16ct::mixed::decode_vec(&record.signature) + .unwrap_or_else(|_| panic!("invalid signature hex: {}", record.signature)) + .try_into() + .expect("invalid signature length"); + + if let Some(seckey) = record.seckey { + let aux_rand = + <[u8; 32]>::try_from(record.aux_rand.as_slice()).unwrap_or_else(|_| { + panic!( + "invalid aux_rand: {}", + base16ct::lower::encode_string(&record.aux_rand) + ) + }); + + let created_signature: CompactSignature = + sign_solo(seckey, &record.message, aux_rand); + + assert_eq!( + created_signature.to_bytes(), + test_vec_signature, + "test vector signature does not match for test vector {}; {}", + record.index, + &record.comment + ); + + // Test adaptor signatures + { + let adaptor_secret = MaybeScalar::Valid(seckey); // arbitrary secret + let adaptor_point = adaptor_secret * G; + let adaptor_signature = + sign_solo_adaptor(seckey, &record.message, aux_rand, adaptor_point); + + verify_single_adaptor( + pubkey, + &adaptor_signature, + &record.message, + adaptor_point, + ) + .expect("failed to verify valid adaptor signature"); + + // Ensure the decrypted signature is valid. + let valid_sig = adaptor_signature.adapt(adaptor_secret).unwrap(); + verify_single(pubkey, valid_sig, &record.message) + .expect("failed to verify decrypted adaptor signature"); + + // Ensure observers can learn the adaptor secret from published signatures. + let revealed: MaybeScalar = adaptor_signature + .reveal_secret(&valid_sig) + .expect("decrypted signature should reveal adaptor secret"); + assert_eq!(revealed, adaptor_secret); + } + } + + let verify_result = verify_single(pubkey, test_vec_signature, &record.message); + match record.verification_result.as_str() { + "TRUE" => { + verify_result.unwrap_or_else(|_| { + panic!( + "verification should pass for signature {} - {}", + &record.signature, record.comment + ) + }); + valid_sigs_batch.push(BatchVerificationRow::from_signature( + pubkey, + record.message, + LiftedSignature::try_from(test_vec_signature).unwrap(), + )); + } + + "FALSE" => { + assert_eq!( + verify_result, + Err(VerifyError::BadSignature), + "verification should fail for signature {} - {}", + &record.signature, + record.comment + ); + } + + s => panic!("unexpected verification result column value: {}", s), + }; + } + + // test batch verification + verify_batch(&valid_sigs_batch).expect("batch verification failed"); + } +} diff --git a/crates/musig2/src/deterministic.rs b/crates/musig2/src/deterministic.rs new file mode 100644 index 00000000..a70e49f1 --- /dev/null +++ b/crates/musig2/src/deterministic.rs @@ -0,0 +1,147 @@ +//! This module provides determinstic BIP340-compatible single-signer logic using +//! [RFC6979](https://www.rfc-editor.org/rfc/rfc6979). +//! +//! This approach produces a synthetic nonce by deriving it from a +//! chained hash of the private key and and the message to be signed. +//! Generating nonces in this way makes signatatures deterministic. +//! +//! Technically RFC6979 is not part of the BIP340 spec, but it is entirely valid +//! to use deterministic nonce generation, provided you can guarantee that the +//! `(seckey, message)` pair are never used for other deterministic signatures +//! outside of BIP340. +//! +//! This is safe in a single-signer environment only (not for MuSig). +//! For deterministic nonces in a multi-signer environment, you will need +//! zero-knowledge proofs. See [this paper for details](https://eprint.iacr.org/2020/1057.pdf). +use secp::{MaybePoint, Scalar}; + +use crate::{AdaptorSignature, LiftedSignature}; + +use hmac::digest::FixedOutput as _; +use hmac::Mac as _; +use sha2::Digest as _; + +fn hmac_sha256(key: &[u8; 32], msg: &[u8]) -> [u8; 32] { + hmac::Hmac::::new_from_slice(key.as_ref()) + .expect("Hmac::new_from_slice never fails") + .chain_update(msg) + .finalize_fixed() + .into() +} + +/// Derive a nonce from a given `(seckey, message)` pair. Follows the procedure +/// laid out in [this section of the RFC](https://www.rfc-editor.org/rfc/rfc6979#section-3.2). +pub fn derive_nonce_rfc6979(seckey: impl Into, message: impl AsRef<[u8]>) -> Scalar { + let seckey = seckey.into(); + + let h1 = sha2::Sha256::new() + .chain_update(message.as_ref()) + .finalize(); + + let mut V = [1u8; 32]; + let mut K = [0u8; 32]; + + // Step D: + // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1)) + let mut buf = vec![0u8; 32 + 1 + 32 + 32]; + buf[..32].copy_from_slice(&V); + buf[32] = 0; + buf[33..65].copy_from_slice(&seckey.serialize()); + buf[65..].copy_from_slice(&h1); + K = hmac_sha256(&K, &buf); + + // Step E: + // V = HMAC_K(V) + V = hmac_sha256(&K, &V); + + // Step F: + // K = HMAC_K(V || 0x01 || int2octets(x) || bits2octets(h1)) + buf[..32].copy_from_slice(&V); + buf[32] = 1; + K = hmac_sha256(&K, &buf); + + // Step G: + // V = HMAC_K(V) + V = hmac_sha256(&K, &V); + + loop { + // Step H2: + // V = HMAC_K(V) + V = hmac_sha256(&K, &V); + + // Step H3: + // k = bits2int(V) + if let Ok(k) = Scalar::from_slice(&V) { + return k; + } + + buf[..32].copy_from_slice(&V); + buf[32] = 0; + K = hmac_sha256(&K, &buf[..33]); + V = hmac_sha256(&K, &V); + } +} + +/// This module provides a determinstic flavor of adaptor signature creation for single-signer contexts. +pub mod adaptor { + use super::*; + + /// This is the same as [`adaptor::sign_solo`][crate::adaptor::sign_solo] except using + /// deterministic nonce generation. + pub fn sign_solo( + seckey: impl Into, + message: impl AsRef<[u8]>, + adaptor_point: impl Into, + ) -> AdaptorSignature { + let seckey = seckey.into(); + let aux = derive_nonce_rfc6979(seckey, &message).serialize(); + crate::adaptor::sign_solo(seckey, message, aux, adaptor_point) + } +} + +/// This is the same as [`sign_solo`][crate::sign_solo] except using deterministic nonce generation. +pub fn sign_solo(seckey: impl Into, message: impl AsRef<[u8]>) -> T +where + T: From, +{ + let seckey = seckey.into(); + let aux = derive_nonce_rfc6979(seckey, &message).serialize(); + crate::sign_solo(seckey, message, aux) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rfc6979_nonces() { + struct TestVector { + seckey: Scalar, + message: &'static str, + expected_nonce: &'static str, + } + + let test_vectors = [ + // from https://www.rfc-editor.org/rfc/rfc6979#appendix-A.2.5 + TestVector { + seckey: "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721" + .parse() + .unwrap(), + message: "sample", + expected_nonce: "A6E3C57DD01ABE90086538398355DD4C3B17AA873382B0F24D6129493D8AAD60", + }, + TestVector { + seckey: "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721" + .parse() + .unwrap(), + message: "test", + expected_nonce: "D16B6AE827F17175E040871A1C7EC3500192C4C92677336EC2537ACAEE0008E0", + }, + ]; + + for test in test_vectors { + let nonce = derive_nonce_rfc6979(test.seckey, test.message); + assert_eq!(format!("{:X}", nonce), test.expected_nonce); + } + } +} diff --git a/crates/musig2/src/errors.rs b/crates/musig2/src/errors.rs new file mode 100644 index 00000000..d30ab8b7 --- /dev/null +++ b/crates/musig2/src/errors.rs @@ -0,0 +1,386 @@ +//! Various error types for different kinds of failures. + +use crate::KeyAggContext; + +use std::error::Error; +use std::fmt; + +/// Returned when aggregating a collection of public keys with [`KeyAggContext`] +/// results in the point at infinity. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct KeyAggError; +impl fmt::Display for KeyAggError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("computed an invalid aggregated key from a collection of public keys") + } +} +impl Error for KeyAggError {} +impl From for KeyAggError { + fn from(_: secp::errors::InfinityPointError) -> Self { + KeyAggError + } +} + +/// Returned when aggregating a collection of secret keys with [`KeyAggContext`], +/// but some secret keys are missing, or the keys are not the correct secret keys +/// for the pubkeys contained in the key agg context. +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct InvalidSecretKeysError; +impl fmt::Display for InvalidSecretKeysError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("missing or invalid secret keys provided for aggregation") + } +} +impl Error for InvalidSecretKeysError {} +impl From for InvalidSecretKeysError { + fn from(_: secp::errors::ZeroScalarError) -> Self { + InvalidSecretKeysError + } +} + +/// Returned when tweaking a [`KeyAggContext`] results in the point +/// at infinity, or if using [`KeyAggContext::with_taproot_tweak`] +/// when the tweak input results in a hash which exceeds the curve +/// order (exceedingly unlikely)" +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct TweakError; +impl fmt::Display for TweakError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("tweak value is invalid") + } +} +impl Error for TweakError {} +impl From for TweakError { + fn from(_: secp::errors::InfinityPointError) -> Self { + TweakError + } +} + +/// Returned when passing a signer index which is out of range for a +/// group of signers +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct SignerIndexError { + /// The index of the signer we did not expect to receive. + pub index: usize, + + /// The total size of the signing group. + pub n_signers: usize, +} +impl fmt::Display for SignerIndexError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "signer index {} is out of range for group of {} signers", + self.index, self.n_signers + ) + } +} +impl Error for SignerIndexError {} + +impl SignerIndexError { + /// Construct a new `SignerIndexError` indicating we received an + /// invalid index for the given group size of signers. + pub(crate) fn new(index: usize, n_signers: usize) -> SignerIndexError { + SignerIndexError { index, n_signers } + } +} + +/// Error returned when (partial) signing fails. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SigningError { + /// Indicates an unknown secret key was provided when + /// using [`sign_partial`][crate::sign_partial] or + /// finalizing the [`FirstRound`][crate::FirstRound]. + UnknownKey, + + /// We could not verify the signature we produced. + /// This may indicate a malicious actor attempted to make us + /// produce a signature which could reveal our secret key. The + /// signing session should be aborted and retried with new nonces. + SelfVerifyFail, +} +impl fmt::Display for SigningError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "failed to create signature: {}", + match self { + Self::UnknownKey => "signing key is not a member of the group", + Self::SelfVerifyFail => "failed to verify our own signature; something is wrong", + } + ) + } +} +impl Error for SigningError {} + +/// Error returned when verification fails. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum VerifyError { + /// Indicates a public key was provided which is not + /// a member of the signing group, and thus partial + /// signature verification on this key has no meaning. + UnknownKey, + + /// The signature is not valid for the given key and message. + BadSignature, +} +impl fmt::Display for VerifyError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "failed to verify signature: {}", + match self { + Self::UnknownKey => "public key is not a member of the group", + Self::BadSignature => "signature is invalid", + } + ) + } +} +impl Error for VerifyError {} + +impl From for SigningError { + fn from(_: VerifyError) -> Self { + SigningError::SelfVerifyFail + } +} + +/// Enumerates the causes for why receiving a contribution from a peer +/// might fail. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ContributionFaultReason { + /// The signer's index is out of range for the given + /// number of signers in the group. Embeds `n_signers` + /// (the number of signers). + OutOfRange(usize), + + /// Indicates we received different contribution values from + /// this peer for the same round. If we receive the same + /// nonce or signature from this peer more than once this is + /// acceptable and treated as a no-op, but receiving inconsistent + /// contributions from the same signer may indicate there is + /// malicious behavior occurring. + InconsistentContribution, + + /// Indicates we received an invalid partial signature. Only returned by + /// [`SecondRound::receive_signature`][crate::SecondRound::receive_signature]. + InvalidSignature, +} + +/// This error is returned by when a peer provides an invalid contribution +/// to one of the signing rounds. +/// +/// This is either because the signer's index exceeds the maximum, or +/// because we received an invalid contribution from this signer. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct RoundContributionError { + /// The erroneous signer index. + pub index: usize, + + /// The reason why the signer's contribution was rejected. + pub reason: ContributionFaultReason, +} + +impl RoundContributionError { + /// Create a new out of range signer index error. + pub fn out_of_range(index: usize, n_signers: usize) -> RoundContributionError { + RoundContributionError { + index, + reason: ContributionFaultReason::OutOfRange(n_signers), + } + } + + /// Create an error caused by an inconsistent contribution. + pub fn inconsistent_contribution(index: usize) -> RoundContributionError { + RoundContributionError { + index, + reason: ContributionFaultReason::InconsistentContribution, + } + } + + /// Create a new error caused by an invalid partial signature. + pub fn invalid_signature(index: usize) -> RoundContributionError { + RoundContributionError { + index, + reason: ContributionFaultReason::InvalidSignature, + } + } +} + +impl fmt::Display for RoundContributionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ContributionFaultReason::*; + write!( + f, + "invalid signer index {}: {}", + self.index, + match self.reason { + OutOfRange(n_signers) => format!("exceeds max index for {} signers", n_signers), + InconsistentContribution => + "received inconsistent contributions from same signer".to_string(), + InvalidSignature => "received invalid partial signature from peer".to_string(), + } + ) + } +} + +impl Error for RoundContributionError {} + +/// Returned when finalizing [`FirstRound`][crate::FirstRound] or +/// [`SecondRound`][crate::SecondRound] fails. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum RoundFinalizeError { + /// Contributions from all signers in the group are required to finalize + /// a signing round. This error is returned if attempting to finalize + /// a round before all contributions are received. + Incomplete, + + /// Indicates partial signing failed unexpectedly. This is likely because + /// the wrong secret key was provided. Only returned by + /// [`FirstRound::finalize`][crate::FirstRound::finalize]. + SigningError(SigningError), + + /// Indicates the final aggregated signature is invalid. Only returned by + /// [`SecondRound::finalize`][crate::SecondRound::finalize]. + InvalidAggregatedSignature(VerifyError), +} + +impl fmt::Display for RoundFinalizeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "cannot finalize round: {}", + match self { + Self::Incomplete => "not all signers have contributed".to_string(), + Self::SigningError(e) => format!("signing failed, {}", e), + Self::InvalidAggregatedSignature(e) => + format!("could not verify aggregated signature: {}", e), + } + ) + } +} + +impl Error for RoundFinalizeError {} + +impl From for RoundFinalizeError { + fn from(e: SigningError) -> Self { + RoundFinalizeError::SigningError(e) + } +} + +impl From for RoundFinalizeError { + fn from(e: VerifyError) -> Self { + RoundFinalizeError::InvalidAggregatedSignature(e) + } +} + +/// Enumerates the various reasons why binary or hex decoding +/// could fail. +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum DecodeFailureReason { + /// The hex string's format was incorrect, which could mean + /// it either was the wrong length or held invalid characters. + BadHexFormat(base16ct::Error), + + /// The byte slice we tried to deserialize had the wrong length. + BadLength(usize), + + /// The bytes contained coordinates to a point that is not on + /// the secp256k1 curve. + InvalidPoint, + + /// The bytes slice contained a representation of a scalar which + /// is outside the required finite field's range. + InvalidScalar, + + /// Custom error reason. + Custom(String), +} + +/// Returned when decoding a certain data structure of type `T` fails. +/// +/// The type `T` only serves as a compile-time safety check; no +/// data of type `T` is actually owned by this error. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DecodeError { + /// The reason for the decoding failure. + pub reason: DecodeFailureReason, + phantom: std::marker::PhantomData, +} + +impl DecodeError { + /// Construct a new decoding error for type `T` given a cause + /// for the failure. + pub fn new(reason: DecodeFailureReason) -> Self { + DecodeError { + reason, + phantom: std::marker::PhantomData, + } + } + + /// Create a decoding error caused by an incorrect input byte + /// slice length. + pub fn bad_length(size: usize) -> Self { + let reason = DecodeFailureReason::BadLength(size); + DecodeError::new(reason) + } + + /// Create a custom decoding failure. + pub fn custom(s: impl fmt::Display) -> Self { + let reason = DecodeFailureReason::Custom(s.to_string()); + DecodeError::new(reason) + } + + /// Converts the decoding error for one type into that of another type. + pub fn convert(self) -> DecodeError { + DecodeError::new(self.reason) + } +} + +impl fmt::Display for DecodeError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use DecodeFailureReason::*; + + write!( + f, + "error decoding {}: {}", + std::any::type_name::(), + match &self.reason { + BadHexFormat(e) => format!("hex decoding error: {}", e), + BadLength(size) => format!("unexpected length {}", size), + InvalidPoint => secp::errors::InvalidPointBytes.to_string(), + InvalidScalar => secp::errors::InvalidScalarBytes.to_string(), + Custom(s) => s.to_string(), + } + ) + } +} + +impl From for DecodeError { + fn from(_: secp::errors::InvalidPointBytes) -> Self { + DecodeError::new(DecodeFailureReason::InvalidPoint) + } +} + +impl From for DecodeError { + fn from(_: secp::errors::InvalidScalarBytes) -> Self { + DecodeError::new(DecodeFailureReason::InvalidScalar) + } +} + +impl From for DecodeError { + fn from(e: base16ct::Error) -> Self { + DecodeError::new(DecodeFailureReason::BadHexFormat(e)) + } +} + +impl From for DecodeError { + fn from(e: KeyAggError) -> Self { + DecodeError::custom(e) + } +} + +impl From for DecodeError { + fn from(_: TweakError) -> Self { + DecodeError::custom("serialized KeyAggContext contains an invalid tweak") + } +} diff --git a/crates/musig2/src/key_agg.rs b/crates/musig2/src/key_agg.rs new file mode 100644 index 00000000..a112621b --- /dev/null +++ b/crates/musig2/src/key_agg.rs @@ -0,0 +1,988 @@ +use std::collections::HashMap; + +use rkyv::{ + with::{Identity, Map, MapKV}, + Archive, Deserialize, Serialize, +}; +use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; +use sha2::Digest as _; +use subtle::ConstantTimeEq as _; + +use crate::{ + errors::{DecodeError, InvalidSecretKeysError, KeyAggError, TweakError}, + rkyv_wrappers, tagged_hashes, BinaryEncoding, +}; + +/// Represents an aggregated and tweaked public key. +/// +/// A set of pubkeys can be aggregated into a `KeyAggContext` which +/// allows co-signers to cooperatively sign data. +/// +/// `KeyAggContext` is essentially a sequence of pubkeys and tweaks +/// which determine a final aggregated key, with which the whole +/// cohort can cooperatively sign messages. +/// +/// See [`KeyAggContext::with_tweak`] to learn +/// more about tweaking. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub struct KeyAggContext { + /// The aggregated pubkey point `Q`. + #[rkyv(with = rkyv_wrappers::Point)] + pub(crate) pubkey: Point, + + /// The component individual pubkeys in their original order. + #[rkyv(with = Map)] + pub(crate) ordered_pubkeys: Vec, + + /// A map of pubkeys to their indexes in the [`ordered_pubkeys`][Self::ordered_pubkeys] + /// field. + #[rkyv(with = MapKV)] + pub(crate) pubkey_indexes: HashMap, + + /// Cached key aggregation coefficients of individual pubkeys, in the + /// same order as `ordered_pubkeys`. + #[rkyv(with = Map)] + pub(crate) key_coefficients: Vec, + + /// A cache of effective individual pubkeys, i.e. `pubkey * self.key_coefficient(pubkey)`. + #[rkyv(with = Map)] + pub(crate) effective_pubkeys: Vec, + + #[rkyv(with = rkyv_wrappers::Choice)] + pub(crate) parity_acc: subtle::Choice, // false means g=1, true means g=n-1 + #[rkyv(with = rkyv_wrappers::MaybeScalar)] + pub(crate) tweak_acc: MaybeScalar, // None means zero. +} + +impl KeyAggContext { + /// Constructs a key aggregation context for a given set of pubkeys. + /// The order in which the pubkeys are presented by the iterator will be preserved. + /// A specific ordering of pubkeys will uniquely determine the aggregated public key. + /// + /// If the same keys are provided again in a different sorting order, a different + /// aggregated pubkey will result. We recommended to sort keys ahead of time + /// in some deterministic fashion before constructing a `KeyAggContext`. + /// + /// ``` + #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::PublicKey;")] + #[cfg_attr( + all(feature = "k256", not(feature = "secp256k1")), + doc = "use secp::Point as PublicKey;" + )] + /// use musig2::KeyAggContext; + /// + /// let mut pubkeys: [PublicKey; 3] = [ + /// "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" + /// .parse() + /// .unwrap(), + /// "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + /// .parse() + /// .unwrap(), + /// "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" + /// .parse() + /// .unwrap(), + /// ]; + /// + /// let key_agg_ctx = KeyAggContext::new(pubkeys) + /// .expect("error aggregating pubkeys"); + /// + /// pubkeys.sort(); + /// let sorted_key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + /// + /// let pk: PublicKey = key_agg_ctx.aggregated_pubkey(); + /// let pk_sorted: PublicKey = sorted_key_agg_ctx.aggregated_pubkey(); + /// assert_ne!(pk, pk_sorted); + /// ``` + /// + /// Multiple copies of the same public key are also accepted. They will + /// be aggregated together and all signers will be expected to provide + /// valid signatures from their key. + /// + /// Signers will be identified by their index from zero. The first key + /// returned from the `pubkeys` iterator will be signer `0`. The second + /// key will be index `1`, and so on. It is important that the caller can + /// clearly identify every signer, so that they know who to blame if + /// a signing contribution (e.g. a partial signature) is invalid. + pub fn new(pubkeys: I) -> Result + where + I: IntoIterator, + P: Into, + { + let ordered_pubkeys: Vec = pubkeys.into_iter().map(|p| p.into()).collect(); + assert!(!ordered_pubkeys.is_empty(), "received empty set of pubkeys"); + assert!( + ordered_pubkeys.len() <= u32::MAX as usize, + "max number of pubkeys is u32::MAX" + ); + + // If all pubkeys are the same, `pk2` will be set to `None`, indicating + // that every public key `X` should be tweaked with a coefficient `H_agg(L, X)` + // to prevent collisions (See appendix B of the musig2 paper). + let pk2: Option<&Point> = ordered_pubkeys[1..] + .iter() + .find(|pubkey| pubkey != &&ordered_pubkeys[0]); + + let pk_list_hash = hash_pubkeys(&ordered_pubkeys); + + let (effective_pubkeys, key_coefficients): (Vec, Vec) = + ordered_pubkeys + .iter() + .map(|&pubkey| { + let key_coeff = + compute_key_aggregation_coefficient(&pk_list_hash, &pubkey, pk2); + (pubkey * key_coeff, key_coeff) + }) + .unzip(); + + let aggregated_pubkey = MaybePoint::sum(&effective_pubkeys).not_inf()?; + + let pubkey_indexes = HashMap::from_iter( + ordered_pubkeys + .iter() + .copied() + .enumerate() + .map(|(i, pk)| (pk, i)), + ); + + Ok(KeyAggContext { + pubkey: aggregated_pubkey, + ordered_pubkeys, + pubkey_indexes, + key_coefficients, + effective_pubkeys, + parity_acc: subtle::Choice::from(0), + tweak_acc: MaybeScalar::Zero, + }) + } + + /// Tweak the key aggregation context with a specific scalar tweak value. + /// + /// 'Tweaking' is the practice of committing a key to an agreed-upon scalar + /// value, such as a SHA256 hash. In Bitcoin contexts, this is used for + /// [taproot](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) + /// script commitments, or + /// [BIP32 key derivation](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki). + /// + /// Signatures created using the resulting tweaked key aggregation context will be + /// bound to this tweak value. + /// + /// A verifier can later prove that the signer(s) committed to this value + /// if the `tweak` value was itself generated by committing to the public key, + /// e.g. by hashing the aggregated public key. + /// + /// The `is_xonly` argument determines whether the tweak should be applied to + /// the plain aggregated pubkey, or to the even-parity (i.e. x-only) aggregated + /// pubkey. `is_xonly` should be true for applying Bitcoin taproot commitments, + /// and false for applying BIP32 key derivation tweaks. + /// + /// Returns an error if the tweaked public key would be the point at infinity. + /// + /// ``` + #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::{PublicKey, SecretKey};")] + #[cfg_attr( + all(feature = "k256", not(feature = "secp256k1")), + doc = "use secp::{Point as PublicKey, Scalar as SecretKey};" + )] + /// use musig2::KeyAggContext; + /// + /// let pubkeys = [ + /// "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" + /// .parse::() + /// .unwrap(), + /// "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" + /// .parse::() + /// .unwrap(), + /// ]; + /// + /// let key_agg_ctx = KeyAggContext::new(pubkeys) + /// .unwrap() + /// .with_tweak( + /// "7931676703c0865d8b502dcdf1d956e86503796cfeabe33d12a918fbf408da05" + /// .parse::() + /// .unwrap(), + /// false + /// ) + /// .unwrap(); + /// + /// let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey_untweaked(); + /// + /// assert_eq!( + /// aggregated_pubkey.to_string(), + /// "0385eb6101982e142dba553cae437d08a82880fe9a22889c997f8e415a61b7a2d5" + /// ); + pub fn with_tweak(self, tweak: impl Into, is_xonly: bool) -> Result { + if is_xonly { + self.with_xonly_tweak(tweak) + } else { + self.with_plain_tweak(tweak) + } + } + + /// Iteratively applies tweaks to the aggregated pubkey. See [`KeyAggContext::with_tweak`]. + pub fn with_tweaks(mut self, tweaks: I) -> Result + where + I: IntoIterator, + S: Into, + { + for (tweak, is_xonly) in tweaks.into_iter() { + self = self.with_tweak(tweak, is_xonly)?; + } + Ok(self) + } + + /// Same as `self.with_tweak(tweak, false)`. See [`KeyAggContext::with_tweak`]. + pub fn with_plain_tweak(self, tweak: impl Into) -> Result { + let tweak: Scalar = tweak.into(); + + // Q' = Q + t*G + let tweaked_pubkey = (self.pubkey + (tweak * G)).not_inf()?; + + // tacc' = t + tacc + let new_tweak_acc = self.tweak_acc + tweak; + + Ok(KeyAggContext { + pubkey: tweaked_pubkey, + tweak_acc: new_tweak_acc, + ..self + }) + } + + /// Same as `self.with_tweak(tweak, true)`. See [`KeyAggContext::with_tweak`]. + pub fn with_xonly_tweak(self, tweak: impl Into) -> Result { + // if has_even_y(Q): g = 1 (Same as a plain tweak.) + // else: g = n - 1 + if self.pubkey.has_even_y() { + return self.with_plain_tweak(tweak); + } + + let tweak: Scalar = tweak.into(); + + // Q' = g*Q + t*G + // + // Negating the pubkey point Q is the same as multiplying it + // by (n-1), but is much faster. + let tweaked_pubkey = (tweak * G - self.pubkey).not_inf()?; + + // tacc' = g*tacc + t + // + // Negating the tweak accumulator is the same as multiplying it + // by (n-1), but is much faster. + let new_tweak_acc = tweak - self.tweak_acc; + + Ok(KeyAggContext { + pubkey: tweaked_pubkey, + parity_acc: !self.parity_acc, + tweak_acc: new_tweak_acc, + ..self + }) + } + + fn with_taproot_tweak_internal(self, merkle_root: &[u8]) -> Result { + // t = int(H_taptweak(xbytes(P), k)) + let tweak_hash: [u8; 32] = tagged_hashes::TAPROOT_TWEAK_TAG_HASHER + .clone() + .chain_update(self.pubkey.serialize_xonly()) + .chain_update(merkle_root) + .finalize() + .into(); + + let tweak = Scalar::try_from(tweak_hash).map_err(|_| TweakError)?; + self.with_xonly_tweak(tweak) + } + + /// Tweak the key aggregation context with the given tapscript merkle tree root hash. + /// + /// This is used to commit the key aggregation context to a specific tree of Bitcoin + /// taproot scripts, determined by the given `merkle_root` hash. Computing the merkle + /// tree root is outside the scope of this package. See + /// [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) + /// for details of how tapscript merkle trees are constructed. + /// + /// The tweak value `t` is computed as: + /// + /// ```notrust + /// prefix = sha256(b"TapTweak") + /// tweak_hash = sha256( + /// prefix, + /// prefix, + /// self.aggregated_pubkey().serialize_xonly(), + /// merkle_root + /// ) + /// t = int(tweak_hash) + /// ``` + /// + /// Note that the _current tweaked aggregated pubkey_ is hashed, not + /// the plain untweaked pubkey. + pub fn with_taproot_tweak(self, merkle_root: &[u8; 32]) -> Result { + self.with_taproot_tweak_internal(merkle_root.as_ref()) + } + + /// Tweak the key aggregation context with an empty unspendable merkle root. + /// + /// This allows a 3rd party observer (who doesn't know the constituent musig group + /// member keys) to verify, given the untweaked group key, that the tweaked group + /// key does not commit to any hidden tapscript trees. See [BIP341 for more info][BIP341]. + /// + /// The tweak value `t` is computed as: + /// + /// ```notrust + /// prefix = sha256(b"TapTweak") + /// tweak_hash = sha256( + /// prefix, + /// prefix, + /// self.aggregated_pubkey().serialize_xonly(), + /// ) + /// t = int(tweak_hash) + /// ``` + /// + /// Note that the _current tweaked aggregated pubkey_ is hashed, not + /// the plain untweaked pubkey. + /// + /// [BIP341]: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23 + pub fn with_unspendable_taproot_tweak(self) -> Result { + self.with_taproot_tweak_internal(b"") + } + + /// Returns the aggregated public key, converted to a given type. + /// + /// ``` + #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::PublicKey;")] + #[cfg_attr( + all(feature = "k256", not(feature = "secp256k1")), + doc = "use secp::Point as PublicKey;" + )] + /// use musig2::KeyAggContext; + /// + /// let pubkeys: Vec = vec![ + /// /* ... */ + /// # "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" + /// # .parse() + /// # .unwrap(), + /// # "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + /// # .parse() + /// # .unwrap(), + /// # "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" + /// # .parse() + /// # .unwrap(), + /// ]; + /// + /// let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + /// let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); + /// assert_eq!( + /// aggregated_pubkey.to_string(), + /// "0290539eede565f5d054f32cc0c220126889ed1e5d193baf15aef344fe59d4610c" + /// ) + /// ``` + /// + /// If any tweaks have been applied to the `KeyAggContext`, the the pubkey + /// returned by this method will be the tweaked aggregate public key, and + /// not the plain aggregated key. + pub fn aggregated_pubkey>(&self) -> T { + T::from(self.pubkey) + } + + /// Returns the aggregated pubkey without any tweaks. + /// + /// ``` + #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::{PublicKey, SecretKey};")] + #[cfg_attr( + all(feature = "k256", not(feature = "secp256k1")), + doc = "use secp::{Point as PublicKey, Scalar as SecretKey};" + )] + /// use musig2::KeyAggContext; + /// + /// let pubkeys = [ + /// /* ... */ + /// # "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" + /// # .parse::() + /// # .unwrap(), + /// # "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" + /// # .parse::() + /// # .unwrap(), + /// ]; + /// + /// let key_agg_ctx = KeyAggContext::new(pubkeys) + /// .unwrap() + /// .with_xonly_tweak( + /// "7931676703c0865d8b502dcdf1d956e86503796cfeabe33d12a918fbf408da05" + /// .parse::() + /// .unwrap() + /// ) + /// .unwrap(); + /// + /// let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey_untweaked(); + /// + /// assert_eq!( + /// aggregated_pubkey, + /// KeyAggContext::new(pubkeys).unwrap().aggregated_pubkey(), + /// ) + /// ``` + pub fn aggregated_pubkey_untweaked>(&self) -> T { + let untweaked = (self.pubkey - self.tweak_acc * G).negate_if(self.parity_acc); + T::from(untweaked.unwrap()) // Can never be infinity + } + + /// Returns the sum of all tweaks applied so far to this `KeyAggContext`. + /// Returns `None` if the tweak sum is zero i.e. if no tweaks have been + /// applied, or if the tweaks canceled each other out (by summing to zero). + pub fn tweak_sum>(&self) -> Option { + self.tweak_acc.into_option().map(T::from) + } + + /// Returns a read-only reference to the ordered set of public keys + /// which this `KeyAggContext` was created with. + pub fn pubkeys(&self) -> &[Point] { + &self.ordered_pubkeys + } + + /// Looks up the index of a given pubkey in the key aggregation group. + /// Returns `None` if the key is not a member of the group. + pub fn pubkey_index(&self, pubkey: impl Into) -> Option { + self.pubkey_indexes.get(&pubkey.into()).copied() + } + + /// Returns the public key for a given signer's index. + /// + /// Keys are best identified by their index from zero, because + /// MuSig allows more than one signer to share the same public key. + pub fn get_pubkey>(&self, index: usize) -> Option { + self.ordered_pubkeys.get(index).copied().map(T::from) + } + + /// Finds the key coefficient for a given public key. Returns `None` if + /// the given `pubkey` is not part of the aggregated key. This coefficient + /// is the same for any two copies of the same public key. + /// + /// Key coefficients are multiplicative tweaks applied to each public key + /// in an aggregated MuSig key. They prevent rogue key attacks by ensuring that + /// signers cannot effectively compute their public key as a function of the + /// pubkeys of other signers. + /// + /// The key coefficient is computed by hashing the public key `X` with a hash of + /// the ordered set of all public keys in the signing group, denoted `L`. + /// `KeyAggContext` caches these coefficients on instantiation. + pub fn key_coefficient(&self, pubkey: impl Into) -> Option { + let index = self.pubkey_index(pubkey)?; + Some(self.key_coefficients[index]) + } + + /// Finds the effective pubkey for a given individual pubkey. This is + /// essentially the same as `pubkey * key_agg_ctx.key_coefficient(pubkey)`, + /// except it is faster than recomputing it manually because the `key_agg_ctx` + /// caches this value internally. + /// + /// Returns `None` if the given `pubkey` is not part of the aggregated key. + pub fn effective_pubkey>(&self, pubkey: impl Into) -> Option { + let index = self.pubkey_index(pubkey)?; + Some(T::from(self.effective_pubkeys[index])) + } + + /// Compute the aggregated secret key for the [`KeyAggContext`] given an ordered + /// set of secret keys. Returns [`InvalidSecretKeysError`] if the secret keys do not + /// align with the ordered set of pubkeys intially given to the [`KeyAggContext`], + /// which can be checked via the [`KeyAggContext::pubkeys`] method. + pub fn aggregated_seckey>( + &self, + seckeys: impl IntoIterator, + ) -> Result { + let mut group_seckey = MaybeScalar::Zero; + for (i, seckey) in seckeys.into_iter().enumerate() { + let key_coeff = *self.key_coefficients.get(i).ok_or(InvalidSecretKeysError)?; + group_seckey += seckey * key_coeff; + } + group_seckey = group_seckey.negate_if(self.parity_acc); + + let group_tweaked_seckey = (group_seckey + self.tweak_acc).not_zero()?; + + if group_tweaked_seckey * G != self.pubkey { + return Err(InvalidSecretKeysError); + } + + Ok(T::from(group_tweaked_seckey)) + } +} + +fn hash_pubkeys>(ordered_pubkeys: &[P]) -> [u8; 32] { + let mut h = tagged_hashes::KEYAGG_LIST_TAG_HASHER.clone(); + for pubkey in ordered_pubkeys { + h.update(pubkey.borrow().serialize()); + } + h.finalize().into() +} + +fn compute_key_aggregation_coefficient( + pk_list_hash: &[u8; 32], + pubkey: &Point, + pk2: Option<&Point>, +) -> MaybeScalar { + if pk2.is_some_and(|pk2| pubkey == pk2) { + return MaybeScalar::one(); + } + + let hash: [u8; 32] = tagged_hashes::KEYAGG_COEFF_TAG_HASHER + .clone() + .chain_update(pk_list_hash) + .chain_update(pubkey.serialize()) + .finalize() + .into(); + + MaybeScalar::reduce_from(&hash) +} + +impl PartialEq for KeyAggContext { + fn eq(&self, other: &Self) -> bool { + self.ordered_pubkeys == other.ordered_pubkeys + && bool::from(self.parity_acc.ct_eq(&other.parity_acc)) + && self.tweak_acc == other.tweak_acc + } +} + +impl Eq for KeyAggContext {} + +impl BinaryEncoding for KeyAggContext { + type Serialized = Vec; + + /// Serializes a key aggregation context object into binary format. + /// + /// This is a variable-length encoding of the following fields: + /// + /// - `header_byte` (1 byte) + /// - Lowest order bit is set if the parity of the aggregated pubkey should be negated upon + /// deserialization (due to use of "x-only" tweaks). + /// - Second lowest order bit is set if there is an accumulated tweak value present in the + /// serialization. + /// - `tweak_acc` \[optional\] (32 bytes) + /// - A non-zero scalar representing the accumulated value of prior tweaks. + /// - Present only if `header_byte & 0b10 != 0`. + /// - `n_pubkey` (4 bytes) + /// - Big-endian encoded `u32`, describing the number of pubkeys which are to follow. + /// - `ordered_pubkeys` (33 * `n_pubkey` bytes) + /// - The public keys needed to reconstruct the `KeyAggContext`, in the same order in which + /// they were originally presented. + /// + /// This is a custom data format, not drawn from any standards. An identical + /// `KeyAggContext` can be reconstructed from this binary representation using + /// [`KeyAggContext::from_bytes`]. + /// + /// This is also the serialization implemented for [`serde::Serialize`] and + /// [`serde::Deserialize`] if the `serde` feature of this crate is enabled. + fn to_bytes(&self) -> Self::Serialized { + let parity_acc_bit = self.parity_acc.unwrap_u8(); + let tweak_acc_bit = u8::from(!self.tweak_acc.is_zero()); + + let n_pubkey = self.ordered_pubkeys.len(); + let total_len = 1 + 4 + (32 * (tweak_acc_bit as usize)) + (n_pubkey * 33); + + let mut serialized = Vec::::with_capacity(total_len); + + let header_byte = (tweak_acc_bit << 1) | parity_acc_bit; + serialized.push(header_byte); + + if tweak_acc_bit != 0 { + serialized.extend_from_slice(&self.tweak_acc.serialize()); + } + + serialized.extend_from_slice(&(n_pubkey as u32).to_be_bytes()); + for pubkey in self.ordered_pubkeys.iter() { + serialized.extend_from_slice(&pubkey.serialize()); + } + + serialized + } + + /// Deserializes a `KeyAggContext` from its binary serialization. + /// See [`KeyAggContext::to_bytes`] for a description of the + /// expected binary format. + fn from_bytes(bytes: &[u8]) -> Result> { + // minimum length: 1 byte header + 4 byte n_pubkey + 33 byte pubkey + if bytes.len() < 38 { + return Err(DecodeError::bad_length(bytes.len())); + } + + let header_byte = bytes[0]; + let parity_acc = subtle::Choice::from(header_byte & 1); + let mut cursor: usize = 1; + + // Decode 32-byte tweak_acc if present + let tweak_acc = if header_byte & 0b10 != 0 { + // only non-zero tweak accumulators are accepted in deserialization + let tweak_acc = Scalar::from_slice(&bytes[cursor..cursor + 32])?; + cursor += 32; + MaybeScalar::Valid(tweak_acc) + } else { + MaybeScalar::Zero + }; + + let n_pubkey_bytes = <[u8; 4]>::try_from(&bytes[cursor..cursor + 4]).unwrap(); + let n_pubkey = u32::from_be_bytes(n_pubkey_bytes) as usize; + cursor += 4; + + // wrong number of bytes remaining for the specified number of pubkeys. + if bytes.len() - cursor != n_pubkey * 33 { + return Err(DecodeError::bad_length(bytes.len())); + } + + let pubkeys: Vec = bytes[cursor..] + .chunks_exact(33) + .map(Point::from_slice) + .collect::>()?; + + let mut key_agg_ctx = KeyAggContext::new(pubkeys)?; + key_agg_ctx.parity_acc = parity_acc; + + if bool::from(parity_acc) { + key_agg_ctx.pubkey = -key_agg_ctx.pubkey; + } + + match tweak_acc { + MaybeScalar::Zero => Ok(key_agg_ctx), + MaybeScalar::Valid(t) => Ok(key_agg_ctx.with_plain_tweak(t)?), + } + } +} + +impl_encoding_traits!(KeyAggContext); +impl_hex_display!(KeyAggContext); + +#[cfg(test)] +mod tests { + use super::*; + use crate::{sign_solo, testhex, verify_single, CompactSignature}; + + #[test] + fn test_key_aggregation() { + const KEY_AGGREGATION_VECTORS: &[u8] = include_bytes!("test_vectors/key_agg_vectors.json"); + + #[derive(serde::Deserialize)] + struct ValidTestCase { + pub key_indices: Vec, + + #[serde(deserialize_with = "testhex::deserialize")] + pub expected: [u8; 32], + } + + #[derive(serde::Deserialize)] + struct KeyAggregationVectors { + #[serde(deserialize_with = "testhex::deserialize_vec")] + pub pubkeys: Vec<[u8; 33]>, + + pub valid_test_cases: Vec, + } + + let vectors: KeyAggregationVectors = serde_json::from_slice(KEY_AGGREGATION_VECTORS) + .expect("failed to load key aggregation test vectors"); + + for test_case in vectors.valid_test_cases { + let pubkeys: Vec = test_case + .key_indices + .into_iter() + .map(|i| { + Point::try_from(&vectors.pubkeys[i]) + .expect("failed to parse valid public key string") + }) + .collect(); + + let aggregated_pubkey: Point = KeyAggContext::new(pubkeys) + .expect("failed to aggregated valid pubkeys") + .aggregated_pubkey(); + + assert_eq!(aggregated_pubkey.serialize_xonly(), test_case.expected); + } + } + + #[test] + fn test_aggregation_context_tweaks() { + let pubkeys: [Point; 3] = [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" + .parse() + .unwrap(), + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" + .parse() + .unwrap(), + "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + .parse() + .unwrap(), + ]; + + let ctx = KeyAggContext::new(pubkeys) + .expect("failed to generate key aggregation context") + .with_xonly_tweak( + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB" + .parse::() + .unwrap(), + ) + .expect("error while tweaking KeyAggContext") + .with_xonly_tweak( + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455" + .parse::() + .unwrap(), + ) + .expect("error while tweaking KeyAggContext") + .with_plain_tweak( + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0" + .parse::() + .unwrap(), + ) + .expect("error while tweaking KeyAggContext") + .with_plain_tweak( + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D" + .parse::() + .unwrap(), + ) + .expect("error while tweaking KeyAggContext"); + + assert_eq!( + ctx.pubkey, + "0269434B39A026A4AAC9E6C1AEBDD3993FFA581C8F7F21B6FAAE15608057F5CE85" + .parse::() + .unwrap() + ); + assert!(bool::from(ctx.parity_acc)); + assert_eq!( + ctx.tweak_acc, + "A5BEB2D09000E2391E98EEBC8AA80CD4FB13845DC75B673D8466609410627D0B" + .parse() + .unwrap() + ); + + // Taproot tweaks + let ctx = KeyAggContext::new(pubkeys) + .expect("failed to generate key aggregation context") + .with_taproot_tweak( + &"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + .parse::() + .unwrap() + .serialize(), + ) + .expect("failed to tweak key with taproot commitment"); + assert_eq!( + ctx.pubkey, + "024650cca5e389f62e960f66ca0400927a7727fc6e84b9c38a1fd9a80271377ceb" + .parse::() + .unwrap() + ); + + // Unspendable tweaks + let ctx = KeyAggContext::new(pubkeys) + .expect("failed to generate key aggregation context") + .with_unspendable_taproot_tweak() + .expect("failed to tweak key with unspendable taproot commitment"); + assert_eq!( + ctx.pubkey, + "029a893e777979e0cb827cd3d0458b1a677ad68f3c69ad0120cf5fc9e3268401cb" + .parse::() + .unwrap() + ); + } + + #[test] + fn key_agg_ctx_serialization() { + struct KeyAggSerializationTest { + pubkeys: Vec<&'static str>, + tweaks: Vec<(&'static str, bool)>, + serialized_hex: &'static str, + } + + let serialization_tests = [ + KeyAggSerializationTest { + pubkeys: vec!["03d6f09ede845037a2396b9877bd6105be437488fad29dcac6576cdb3610f3ab66"], + tweaks: vec![], + serialized_hex: + "000000000103d6f09ede845037a2396b9877bd6105be437488fad29dcac6576cdb3610f3ab66", + }, + KeyAggSerializationTest { + pubkeys: vec![ + "0368288652632d5402d5ae3cb8d4094cb006aa2940156ff5dc4735d1445dfe1b34", + "02c65e684ab27c879f46f47064acf80f2c4590fb6edf7932a79b973246c7edd331", + "02d1b04674aaf6966af91201307b31501c56e6fdc41cd146b9e33912ec6e5182a2", + ], + tweaks: vec![], + serialized_hex: + "00000000030368288652632d5402d5ae3cb8d4094cb006aa2940156ff5dc4735d1445dfe1b\ + 3402c65e684ab27c879f46f47064acf80f2c4590fb6edf7932a79b973246c7edd33102d1b0\ + 4674aaf6966af91201307b31501c56e6fdc41cd146b9e33912ec6e5182a2", + }, + KeyAggSerializationTest { + pubkeys: vec![ + "032dd0f586175a1aa4c2fb9d01ec8d883de009d994f0db6a1f8ff75c4362e50c8a", + "03b68eede67797f8bd6b7d4adf6138942f344c2973e3e88ad254aedece82a144da", + ], + tweaks: vec![( + "79441652a4864a0545fa1588af4e8dd7895ddb45c1cf15a7e05d3e0d9fb86c9b", + true, + )], + serialized_hex: + "0279441652a4864a0545fa1588af4e8dd7895ddb45c1cf15a7e05d3e0d9fb86c9b00000002\ + 032dd0f586175a1aa4c2fb9d01ec8d883de009d994f0db6a1f8ff75c4362e50c8a03b68eed\ + e67797f8bd6b7d4adf6138942f344c2973e3e88ad254aedece82a144da", + }, + KeyAggSerializationTest { + pubkeys: vec![ + "025027dab744f11eafb6529c8f7cb4b2390883b55a76cf412bf49c5e39df755c3e", + "030413712ed74027795832e78457020aeff0e73624327c6d321737881078b780dd", + ], + tweaks: vec![ + ( + "d0f447289a190a50832e5daf723b0d01a58441ca465743d2db8d3c4baae37cc8", + false, + ), + ( + "cd9aa8433e17b3c07c3241592e62e7169aa7ee98cb14e95ec47f41ea4eda9ddf", + false, + ), + ], + serialized_hex: + "029e8eef6bd830be10ff609f08a09df419857d537c62238cf5e03a1fa92987d96600000002\ + 025027dab744f11eafb6529c8f7cb4b2390883b55a76cf412bf49c5e39df755c3e030413712\ + ed74027795832e78457020aeff0e73624327c6d321737881078b780dd", + }, + KeyAggSerializationTest { + pubkeys: vec![ + "0355d7de59c20355f9d8b14ccb60998983e9d73b38f64c9b9f2c4f868c0a7ac02f", + "039582f6f17f99784bc6de6e5664ef5f69eb1bf0dc151d824b19481ab0717c0cd5", + ], + tweaks: vec![ + ( + "55542efaf2708bbde8d36dec4b2ac4a698d30d2320ea4e373e31b79d803a8633", + true, + ), + ( + "09a7200a86d56e24ca0d23b64eb25ba4458b2834ceaa6506319e10e4e605e2db", + false, + ), + ( + "5dc0cf7ce9bf937ccbf6167d0c1ba02b9ae2a615ff52142e3b932d332ab699f4", + true, + ), + ( + "42cc20f9df78fc1ca2fa83d03942bae507f74716d68304d008c54eade89cafd4", + false, + ), + ], + serialized_hex: + "034191a1714ff295b6bc1008aaab813ac5c47bb7d4e64065c0d488b35ead12e0ba\ + 000000020355d7de59c20355f9d8b14ccb60998983e9d73b38f64c9b9f2c4f868c\ + 0a7ac02f039582f6f17f99784bc6de6e5664ef5f69eb1bf0dc151d824b19481ab0717c0cd5", + }, + KeyAggSerializationTest { + pubkeys: vec![ + "0317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc5", + "02947f02de710d51280b861c101bcee4e06f09a5a119694677818dce59354b62a8", + "023b89ea0ef047b6f6a2aa826e869c9538fe2f011f4df5a5422af4c24c19f22856", + ], + tweaks: vec![ + ( + "ffa540e2d3df158dfb202fc1a2cbb20c4920ba35e8f75bb11101bfa47d71449a", + true, + ), + ( + "fdc5d9e884851a8a5dd1e8c2015b15e9aed45807d05eea1b897421770351e09e", + true, + ), + ( + "2743a21ac21cc46843e478ce094663c08103f9ab88c53850f4b3280ded4d75c1", + true, + ), + ], + serialized_hex: + "0229d8874f69b8944feaf2604a651f9bc7fe6ca13b2e0032fbd9e2040c0cf6d30b0000000\ + 30317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc502947f\ + 02de710d51280b861c101bcee4e06f09a5a119694677818dce59354b62a8023b89ea0ef04\ + 7b6f6a2aa826e869c9538fe2f011f4df5a5422af4c24c19f22856", + }, + ]; + + for test_case in serialization_tests { + let pubkeys: Vec = test_case + .pubkeys + .into_iter() + .map(|s| s.parse().unwrap()) + .collect(); + + let tweaks: Vec<(Scalar, bool)> = test_case + .tweaks + .into_iter() + .map(|(s, is_xonly)| (s.parse().unwrap(), is_xonly)) + .collect(); + + let expected_serialization = + base16ct::mixed::decode_vec(test_case.serialized_hex).unwrap(); + + let key_agg_ctx = KeyAggContext::new(pubkeys) + .unwrap() + .with_tweaks(tweaks) + .unwrap(); + + let serialized_ctx = key_agg_ctx.to_bytes(); + assert_eq!( + serialized_ctx, expected_serialization, + "serialized KeyAggContext does not match expected" + ); + + let deserialized_ctx = KeyAggContext::from_bytes(&serialized_ctx) + .expect("error deserializing KeyAggContext"); + + assert_eq!( + deserialized_ctx, key_agg_ctx, + "deserialized KeyAggContext does not match original" + ); + + // Test serde deserialization + let _: KeyAggContext = + serde_json::from_str(&format!("\"{}\"", test_case.serialized_hex)) + .expect("failed to deserialize KeyAggContext with serde"); + } + } + + // The test is repeated to catch failures caused by keys whose + // parity randomly align to make incorrect parity-handling code succeed. + #[test] + fn secret_key_aggregation_random() { + let mut rng = rand::thread_rng(); + for _ in 0..16 { + let seckeys = [ + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + Scalar::random(&mut rng), + ]; + + let pubkeys: Vec = seckeys + .into_iter() + .map(|seckey| seckey.base_point_mul()) + .collect(); + + // Without tweak + { + let key_agg_ctx = KeyAggContext::new(pubkeys.clone()).unwrap(); + let group_seckey: Scalar = key_agg_ctx.aggregated_seckey(seckeys).unwrap(); + let group_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + assert_eq!(group_seckey.base_point_mul(), group_pubkey); + + let message = b"hello world"; + let signature: CompactSignature = sign_solo(group_seckey, message, &mut rng); + + verify_single(group_pubkey, signature, message) + .expect("tweaked signature as group should be valid"); + } + + // With a tweak + { + let key_agg_ctx = KeyAggContext::new(pubkeys.clone()) + .unwrap() + .with_unspendable_taproot_tweak() + .unwrap(); + + let group_seckey: Scalar = key_agg_ctx.aggregated_seckey(seckeys).unwrap(); + let group_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + assert_eq!(group_seckey.base_point_mul(), group_pubkey); + + let message = b"hello world"; + let signature: CompactSignature = sign_solo(group_seckey, message, &mut rng); + + verify_single(group_pubkey, signature, message) + .expect("tweaked signature as group should be valid"); + } + } + } +} diff --git a/crates/musig2/src/key_sort.rs b/crates/musig2/src/key_sort.rs new file mode 100644 index 00000000..6e2ad430 --- /dev/null +++ b/crates/musig2/src/key_sort.rs @@ -0,0 +1,22 @@ +#[cfg(test)] +mod tests { + use secp::Point; + + #[test] + fn test_sort_public_keys() { + const KEY_SORT_VECTORS: &[u8] = include_bytes!("test_vectors/key_sort_vectors.json"); + + #[derive(serde::Deserialize)] + struct KeySortVectors { + pubkeys: Vec, + sorted_pubkeys: Vec, + } + + let vectors: KeySortVectors = serde_json::from_slice(KEY_SORT_VECTORS) + .expect("failed to decode key_sort_vectors.json"); + + let mut pubkeys = vectors.pubkeys; + pubkeys.sort(); + assert_eq!(pubkeys, vectors.sorted_pubkeys); + } +} diff --git a/crates/musig2/src/lib.rs b/crates/musig2/src/lib.rs new file mode 100644 index 00000000..21b33d3b --- /dev/null +++ b/crates/musig2/src/lib.rs @@ -0,0 +1,55 @@ +#![doc = include_str!("../README.md")] +#![doc = include_str!("../doc/API.md")] +#![allow(non_snake_case)] +#![warn(missing_docs)] + +#[cfg(all(not(feature = "secp256k1"), not(feature = "k256")))] +compile_error!("At least one of the `secp256k1` or `k256` features must be enabled."); + +#[macro_use] +mod binary_encoding; + +mod bip340; +mod key_agg; +mod key_sort; +mod nonces; +mod rkyv_wrappers; +mod rounds; +mod sig_agg; +mod signature; +mod signing; + +#[doc = include_str!("../doc/adaptor_signatures.md")] +pub mod adaptor { + pub use crate::{ + bip340::{sign_solo_adaptor as sign_solo, verify_single_adaptor as verify_single}, + sig_agg::aggregate_partial_adaptor_signatures as aggregate_partial_signatures, + signature::AdaptorSignature, + signing::{sign_partial_adaptor as sign_partial, verify_partial_adaptor as verify_partial}, + }; +} + +pub mod deterministic; +pub mod errors; +pub mod tagged_hashes; + +pub use binary_encoding::*; +pub use bip340::{sign_solo, verify_single}; +pub use key_agg::*; +pub use nonces::*; +pub use rounds::*; +pub use sig_agg::aggregate_partial_signatures; +pub use signature::*; +pub use signing::{compute_challenge_hash_tweak, sign_partial, verify_partial, PartialSignature}; + +#[cfg(test)] +pub(crate) mod testhex; + +#[cfg(any(test, feature = "rand"))] +pub use bip340::{verify_batch, BatchVerificationRow}; +#[cfg(feature = "k256")] +pub use k256; +/// Re-export of the inner types used to represent curve points and scalars. +pub use secp; +#[cfg(feature = "secp256k1")] +pub use secp256k1; diff --git a/crates/musig2/src/nonces.rs b/crates/musig2/src/nonces.rs new file mode 100644 index 00000000..f4974e2f --- /dev/null +++ b/crates/musig2/src/nonces.rs @@ -0,0 +1,992 @@ +use rkyv::{Archive, Deserialize, Serialize}; +use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; +// use serde::{Deserialize, Serialize}; +use sha2::Digest as _; + +use crate::{errors::DecodeError, rkyv_wrappers, tagged_hashes, BinaryEncoding}; + +/// Represents the primary source of entropy for building a [`SecNonce`]. +/// +/// Often referred to as the variable `rand` in +/// [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) and +/// [BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) +pub struct NonceSeed(pub [u8; 32]); + +impl From<[u8; 32]> for NonceSeed { + /// Converts a byte array to a `NonceSeed` by moving. + fn from(bytes: [u8; 32]) -> Self { + NonceSeed(bytes) + } +} + +impl From<&[u8; 32]> for NonceSeed { + /// Converts a reference to a byte array to a `NonceSeed` by copying. + fn from(bytes: &[u8; 32]) -> Self { + NonceSeed(*bytes) + } +} + +#[cfg(any(test, feature = "rand"))] +impl From<&mut T> for NonceSeed { + /// This implementation draws a [`NonceSeed`] from a mutable reference + /// to a CSPRNG. Panics if the RNG fails to fill the seed with 32 + /// random bytes. + fn from(rng: &mut T) -> NonceSeed { + let mut bytes = [0u8; 32]; + rng.try_fill_bytes(&mut bytes) + .expect("error generating secure secret nonce seed"); + NonceSeed(bytes) + } +} + +pub(crate) fn xor_bytes(a: &[u8; SIZE], b: &[u8; SIZE]) -> [u8; SIZE] { + let mut out = [0; SIZE]; + for i in 0..SIZE { + out[i] = a[i] ^ b[i] + } + out +} + +fn extra_input_length_check>(extra_inputs: &[T]) { + let total_len: usize = extra_inputs + .iter() + .map(|extra_input| extra_input.as_ref().len()) + .sum(); + assert!( + total_len <= u32::MAX as usize, + "excessive use of extra_input when building secnonce; max length is 2^32 bytes" + ); +} + +/// A set of optional parameters which can be provided to _spice up_ the +/// entropy of the secret nonces generated for a signing session. +/// +/// These parameters are not functionally required for any operations after +/// nonce generation - you can provide a different secret key in the `SecNonceSpices` +/// than you'll use for actual signing, and the signature will still be valid. +/// However, using the parameters appropriately will reduce the risk of +/// your code accidentally reusing a nonce and exposing your secret key. +/// +/// This type is meant to be used as a parameter of the state-machine API available +/// via [`FirstRound`][crate::FirstRound] and [`SecondRound`][crate::SecondRound]. +/// For standalone nonce generation, see [`SecNonceBuilder`] or [`SecNonce::generate`]. +#[derive(Clone, Default)] +pub struct SecNonceSpices<'ns> { + pub(crate) seckey: Option, + pub(crate) message: Option<&'ns dyn AsRef<[u8]>>, + pub(crate) extra_inputs: Vec<&'ns dyn AsRef<[u8]>>, +} + +impl<'ns> SecNonceSpices<'ns> { + /// Creates a new empty set of `SecNonceSpices`. Same as [`SecNonceSpices::default`]. + pub fn new() -> SecNonceSpices<'ns> { + SecNonceSpices::default() + } + + /// Add the secret key you intend to sign with to the spice rack. + /// This doesn't _need_ to be the actual key you sign with, but + /// for best efficacy that would be the recommended usage. + pub fn with_seckey(self, seckey: impl Into) -> SecNonceSpices<'ns> { + SecNonceSpices { + seckey: Some(seckey.into()), + ..self + } + } + + /// Spices up the nonce with the message you intend to sign. Similarly + /// to [`SecNonceSpices::with_seckey`], this doesn't need to be the actual message + /// you end up signing, but that would help. + pub fn with_message>(self, message: &'ns M) -> SecNonceSpices<'ns> { + SecNonceSpices { + message: Some(message), + ..self + } + } + + /// Add some arbitrary extra input, any context-specific data you have on hand, to + /// spice up the nonce generation process. This method is additive, appending + /// further extra data on top of previous chunks, which will all be cumulatively + /// hashed to produce the final secret nonce. + /// + /// ``` + /// let session_id = [0x11u8; 16]; + /// + /// musig2::SecNonceSpices::new() + /// .with_extra_input(b"hello world") + /// .with_extra_input(&session_id) + /// .with_extra_input(&(42u32).to_be_bytes()); + /// ``` + pub fn with_extra_input>(mut self, extra_input: &'ns E) -> SecNonceSpices<'ns> { + self.extra_inputs.push(extra_input); + extra_input_length_check(&self.extra_inputs); + self + } +} + +/// A helper struct used to construct [`SecNonce`] instances. +/// +/// `SecNonceBuilder` allows piecemeal salting of the resulting `SecNonce` +/// depending on what is available to the caller. +/// +/// `SecNonce`s can be constructed in a variety of ways using different +/// input sources to increase their entropy. While simple random sampling +/// of `SecNonce` is acceptable in theory, RNGs can fail quietly sometimes. +/// If possible, it is highly recommended to also salt the nonce with +/// session-specific data, such as the message being signed, or the +/// public/secret key which will be used for signing. +/// +/// At bare minimum, [`SecNonceBuilder::new`] requires only 32 random +/// input bytes. Chainable methods can be used thereafter to salt the resulting +/// nonce with additional data. The nonce can be finalized and returned by +/// [`SecNonceBuilder::build`]. +/// +/// If no other data is available, we highly recommend _at least_ salting the nonce +/// with the public key, as recommended by BIP327. +/// +/// # Example +/// +/// Here we construct a nonce which we intend to use to sign the byte string +/// `b"hello world"` with a specific public key. +/// +/// ``` +/// use secp::Point; +/// +/// // in reality, this would be generated by a CSPRNG. +/// let nonce_seed = [0xAB; 32]; +/// +/// let secnonce = musig2::SecNonceBuilder::new(nonce_seed) +/// .with_pubkey( +/// "037eaef9ce945fbcef58c6ca818f433fad8275c09441b06a274a93aa5d69374f62" +/// .parse::() +/// .expect("fail"), +/// ) +/// .with_message(b"hello world") +/// .build(); +/// +/// assert_eq!( +/// secnonce, +/// "304e472f8028efc386eb305b496e49a9c71984fbddb915c04002764a98d77a82\ +/// b2f29921753a6a05a1f91556debdaac4d20ad20519f91bcebf4a2d842a05b0bc" +/// .parse() +/// .unwrap() +/// ); +/// ``` +pub struct SecNonceBuilder<'snb> { + nonce_seed_bytes: [u8; 32], + seckey: Option, + pubkey: Option, + aggregated_pubkey: Option, + message: Option<&'snb [u8]>, + extra_inputs: Vec<&'snb dyn AsRef<[u8]>>, +} + +impl<'snb> SecNonceBuilder<'snb> { + /// Start building a nonce, seeded with the given random data + /// source `nonce_seed`, which should either be + /// + /// - 32 bytes drawn from a cryptographically secure RNG, OR + /// - a mutable reference to a secure RNG. + /// + /// ``` + /// use rand::RngCore as _; + /// + /// # #[cfg(feature = "rand")] + /// // Sample the seed automatically + /// let secnonce = musig2::SecNonceBuilder::new(&mut rand::rngs::OsRng) + /// .with_message(b"hello world!") + /// .build(); + /// + /// // Sample the seed manually + /// let mut nonce_seed = [0u8; 32]; + /// rand::rngs::OsRng.fill_bytes(&mut nonce_seed); + /// let secnonce = musig2::SecNonceBuilder::new(nonce_seed) + /// .with_message(b"hello world!") + /// .build(); + /// ``` + /// + /// # WARNING + /// + /// It is critical for the `nonce_seed` to be **sampled randomly,** and NOT + /// constructed deterministically based on signing session data. Otherwise, + /// the signer can be [tricked into reusing the same nonce for concurrent + /// signing sessions, thus exposing their secret key.]( + #[doc = "https://medium.com/blockstream/musig-dn-schnorr-multisignatures\ + -with-verifiably-deterministic-nonces-27424b5df9d6#e3b6)"] + pub fn new(nonce_seed: impl Into) -> SecNonceBuilder<'snb> { + let NonceSeed(nonce_seed_bytes) = nonce_seed.into(); + SecNonceBuilder { + nonce_seed_bytes, + seckey: None, + pubkey: None, + aggregated_pubkey: None, + message: None, + extra_inputs: Vec::new(), + } + } + + /// Salt the resulting nonce with the public key expected to be used + /// during the signing phase. + /// + /// The public key will be overwritten if [`SecNonceBuilder::with_seckey`] + /// is used after this method. + pub fn with_pubkey(self, pubkey: impl Into) -> SecNonceBuilder<'snb> { + SecNonceBuilder { + pubkey: Some(pubkey.into()), + ..self + } + } + + /// Salt the resulting nonce with the secret key which the nonce should be + /// used to protect during the signing phase. + /// + /// Overwrites any public key previously added by + /// [`SecNonceBuilder::with_pubkey`], as we compute the public key + /// of the given secret key and add it to the builder. + pub fn with_seckey(self, seckey: impl Into) -> SecNonceBuilder<'snb> { + let seckey: Scalar = seckey.into(); + SecNonceBuilder { + seckey: Some(seckey), + pubkey: Some(seckey * G), + ..self + } + } + + /// Salt the resulting nonce with the message which we expect to be signing with + /// the nonce. + pub fn with_message>(self, msg: &'snb M) -> SecNonceBuilder<'snb> { + SecNonceBuilder { + message: Some(msg.as_ref()), + ..self + } + } + + /// Salt the resulting nonce with the aggregated public key which we expect to aggregate + /// signatures for. + pub fn with_aggregated_pubkey( + self, + aggregated_pubkey: impl Into, + ) -> SecNonceBuilder<'snb> { + SecNonceBuilder { + aggregated_pubkey: Some(aggregated_pubkey.into()), + ..self + } + } + + /// Salt the resulting nonce with arbitrary extra input bytes. This might be context-specific + /// data like a signing session ID, the name of the protocol, the current timestamp, whatever + /// you want, really. + /// + /// This method is additive; it does not overwrite the `extra_input` values added by previous + /// invocations of itself. This allows the caller to salt the nonce with an arbitrary amount + /// of extra entropy as desired, up to a limit of [`u32::MAX`] bytes (about 4GB). This method + /// will panic if the sum of all extra inputs attached to the builder would exceed that limit. + /// + /// ``` + /// # let nonce_seed = [0xABu8; 32]; + /// let remote_ip = [127u8, 0, 0, 1]; + /// + /// let secnonce = musig2::SecNonceBuilder::new(nonce_seed) + /// .with_extra_input(b"MyApp") + /// .with_extra_input(&remote_ip) + /// .with_extra_input(&String::from("What's up buttercup?")) + /// .build(); + /// ``` + pub fn with_extra_input>( + mut self, + extra_input: &'snb E, + ) -> SecNonceBuilder<'snb> { + self.extra_inputs.push(extra_input); + extra_input_length_check(&self.extra_inputs); + self + } + + /// Sprinkles in a set of [`SecNonceSpices`] to this nonce builder. Extra inputs in + /// `spices` are appended to the builder (see [`SecNonceBuilder::with_extra_input`]). + /// All other parameters will be merged with those in `spices`, preferring parameters + /// in `spices` if they are present. + pub fn with_spices(mut self, spices: SecNonceSpices<'snb>) -> SecNonceBuilder<'snb> { + self.seckey = spices.seckey.or(self.seckey); + self.message = spices.message.map(|msg| msg.as_ref()).or(self.message); + + let mut new_extra_inputs = spices.extra_inputs; + self.extra_inputs.append(&mut new_extra_inputs); + extra_input_length_check(&self.extra_inputs); + + self + } + + /// Build the secret nonce by hashing all of the builder's inputs into two + /// byte arrays, and reducing those byte arrays modulo the curve order into + /// two scalars `k1` and `k2`. These form the `SecNonce` as the tuple `(k1, k2)`. + /// + /// If the reduction results in an output of zero for either scalar, + /// we use a nonce of 1 instead for that scalar. + /// + /// This method matches the standard nonce generation algorithm specified in + /// [BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki), + /// except in the extremely unlikely case of a hash reducing to zero. + pub fn build(self) -> SecNonce { + let seckey_bytes = match self.seckey { + Some(seckey) => seckey.serialize(), + None => [0u8; 32], + }; + + let nonce_seed_hash: [u8; 32] = tagged_hashes::MUSIG_AUX_TAG_HASHER + .clone() + .chain_update(self.nonce_seed_bytes) + .finalize() + .into(); + + let mut hasher = tagged_hashes::MUSIG_NONCE_TAG_HASHER + .clone() + .chain_update(xor_bytes(&seckey_bytes, &nonce_seed_hash)); + + // BIP327 doesn't allow the public key to be an optional argument, + // but there is no hard reason for that other than 'the RNG might fail'. + // For ergonomics we allow the pubkey to be omitted here in the same + // fashion as the aggregated pubkey. + match self.pubkey { + None => hasher.update([0]), + Some(pubkey) => { + hasher.update([33]); // individual pubkey len + hasher.update(pubkey.serialize()); + } + } + + match self.aggregated_pubkey { + None => hasher.update([0]), + Some(aggregated_pubkey) => { + hasher.update([32]); // aggregated pubkey len + hasher.update(aggregated_pubkey.serialize_xonly()); + } + }; + + match self.message { + None => hasher.update([0]), + Some(message) => { + hasher.update([1]); + hasher.update((message.len() as u64).to_be_bytes()); + hasher.update(message); + } + }; + + // We still write the extra input length if the caller provided empty extra info. + if !self.extra_inputs.is_empty() { + let extra_input_total_len: usize = self + .extra_inputs + .iter() + .map(|extra_in| extra_in.as_ref().len()) + .sum(); + + hasher.update((extra_input_total_len as u32).to_be_bytes()); + for extra_input in self.extra_inputs { + hasher.update(extra_input.as_ref()); + } + } + + // Cloning the hash engine state reduces the computations needed. + let hash1 = <[u8; 32]>::from(hasher.clone().chain_update([0]).finalize()); + let hash2 = <[u8; 32]>::from(hasher.clone().chain_update([1]).finalize()); + + let k1 = match MaybeScalar::reduce_from(&hash1) { + MaybeScalar::Zero => Scalar::one(), + MaybeScalar::Valid(k) => k, + }; + let k2 = match MaybeScalar::reduce_from(&hash2) { + MaybeScalar::Zero => Scalar::one(), + MaybeScalar::Valid(k) => k, + }; + SecNonce { k1, k2 } + } +} + +/// A pair of secret nonce scalars, used to conceal a secret key when +/// signing a message. +/// +/// The secret nonce provides randomness, blinding a signer's private key when +/// signing. It is imperative that the same `SecNonce` is not used to sign more +/// than one message with the same key, as this would allow an observer to +/// compute the private key used to create both signatures. +/// +/// `SecNonce`s can be constructed in a variety of ways using different +/// input sources to increase their entropy. See [`SecNonceBuilder`] and +/// [`SecNonce::build`] to explore secure nonce generation using +/// contextual entropy sources. +/// +/// Ideally, `SecNonce`s should be generated with a cryptographically secure +/// random number generator via [`SecNonce::generate`]. +#[derive(Debug, Eq, PartialEq, Clone, Archive, Serialize, Deserialize)] +pub struct SecNonce { + #[rkyv(with = rkyv_wrappers::Scalar)] + pub(crate) k1: Scalar, + #[rkyv(with = rkyv_wrappers::Scalar)] + pub(crate) k2: Scalar, +} + +impl SecNonce { + /// Construct a new `SecNonce` from the given individual nonce values. + pub fn new>(k1: T, k2: T) -> SecNonce { + SecNonce { + k1: k1.into(), + k2: k2.into(), + } + } + + /// Constructs a new [`SecNonceBuilder`] from the given random nonce seed. + /// + /// See [`SecNonceBuilder::new`]. + pub fn build<'snb>(nonce_seed: impl Into) -> SecNonceBuilder<'snb> { + SecNonceBuilder::new(nonce_seed) + } + + /// Generates a `SecNonce` securely from the given input arguments. + /// + /// - `nonce_seed`: the primary source of entropy used to generate the nonce. Can be any type + /// that converts to [`NonceSeed`], such as [`&mut rand::rngs::OsRng`][rand::rngs::OsRng] or + /// `[u8; 32]`. + /// - `seckey`: the secret key which will be used to sign the message. + /// - `aggregated_pubkey`: the aggregated public key. + /// - `message`: the message which will be signed. + /// - `extra_input`: arbitrary context data used to increase the entropy of the resulting + /// nonces. + /// + /// This implementation matches the specfication of nonce generation in BIP327, + /// and all arguments are required. If you cannot supply all arguments + /// to the nonce generation algorithm, use [`SecNonceBuilder`]. + /// + /// Panics if the extra input length is greater than [`u32::MAX`]. + pub fn generate( + nonce_seed: impl Into, + seckey: impl Into, + aggregated_pubkey: impl Into, + message: impl AsRef<[u8]>, + extra_input: impl AsRef<[u8]>, + ) -> SecNonce { + Self::build(nonce_seed) + .with_seckey(seckey) + .with_aggregated_pubkey(aggregated_pubkey) + .with_message(&message) + .with_extra_input(&extra_input) + .build() + } + + /// Samples a random pair of secret nonces directly from a CSPRNG. + /// + /// Whenever possible, we recommended to use [`SecNonce::generate`] or + /// [`SecNonceBuilder`] instead of this method. If the RNG fails silently for + /// any reason, it may result in duplicate `SecNonce` values, which will lead + /// to private key exposure if this same nonce is used in more than one signing + /// session. + /// + /// [`SecNonce::generate`] is more secure because it combines multiple sources of + /// entropy to compute the final nonce. + #[cfg(any(test, feature = "rand"))] + pub fn random(rng: &mut R) -> SecNonce + where + R: rand::RngCore + rand::CryptoRng, + { + SecNonce { + k1: Scalar::random(rng), + k2: Scalar::random(rng), + } + } + + /// Returns the corresponding public nonce for this secret nonce. The public nonce + /// is safe to share with other signers. + pub fn public_nonce(&self) -> PubNonce { + PubNonce { + R1: self.k1 * G, + R2: self.k2 * G, + } + } +} + +/// Represents a public nonce derived from a secret nonce. It is composed +/// of two public points, `R1` and `R2`, derived by base-point multiplying +/// the two scalars in a `SecNonce`. +/// +/// `PubNonce` can be derived from a [`SecNonce`] using [`SecNonce::public_nonce`], +/// or it can be constructed manually with [`PubNonce::new`]. +#[derive(Debug, Eq, PartialEq, Clone, Hash, Ord, PartialOrd, Archive, Serialize, Deserialize)] +pub struct PubNonce { + #[allow(missing_docs)] + #[rkyv(with = rkyv_wrappers::Point)] + pub R1: Point, + #[allow(missing_docs)] + #[rkyv(with = rkyv_wrappers::Point)] + pub R2: Point, +} + +impl PubNonce { + /// Construct a new `PubNonce` from the given pair of public nonce points. + pub fn new>(R1: T, R2: T) -> PubNonce { + PubNonce { + R1: R1.into(), + R2: R2.into(), + } + } +} + +/// Represents a aggregate sum of public nonces derived from secret nonces. +/// +/// `AggNonce` can be created by summing a collection of `PubNonce` points +/// by using [`AggNonce::sum`] or by making use of the +/// [`std::iter::Sum`](#impl-Sum

-for-AggNonce) implementation. An aggregated +/// nonce can also be constructed directly by using [`AggNonce::new`]. +/// +/// An aggregated nonce's points are allowed to be infinity (AKA the zero point). +/// If this occurs, then likely at least one signer is being mischevious. +/// To allow honest signers to identify those responsible, signing is allowed +/// to continue, and dishonest signers will reveal themselves once they are +/// required to provide their partial signatures. +#[derive(Debug, Eq, PartialEq, Clone, Hash, Ord, PartialOrd, Archive, Serialize, Deserialize)] +pub struct AggNonce { + #[allow(missing_docs)] + #[rkyv(with = rkyv_wrappers::MaybePoint)] + pub R1: MaybePoint, + #[allow(missing_docs)] + #[rkyv(with = rkyv_wrappers::MaybePoint)] + pub R2: MaybePoint, +} + +impl AggNonce { + /// Construct a new `AggNonce` from the given pair of public nonce points. + pub fn new>(R1: T, R2: T) -> AggNonce { + AggNonce { + R1: R1.into(), + R2: R2.into(), + } + } + + /// Aggregates many partial public nonces together into an aggregated nonce. + /// + /// ``` + /// use musig2::{AggNonce, PubNonce}; + /// + /// let nonces: [PubNonce; 2] = [ + /// "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798\ + /// 032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE93" + /// .parse() + /// .unwrap(), + /// "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61\ + /// 037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9" + /// .parse() + /// .unwrap(), + /// ]; + /// + /// let expected = "02aebee092fe428c3b4c53993c3f80eecbf88ca935469b5bfcaabecb7b2afbb1a6\ + /// 03c923248ac1f639368bc82345698dfb445dca6024b9ba5a9bafe971bb5813964b" + /// .parse::() + /// .unwrap(); + /// + /// assert_eq!(musig2::AggNonce::sum(&nonces), expected); + /// assert_eq!(musig2::AggNonce::sum(nonces), expected); + /// ``` + pub fn sum(nonces: I) -> AggNonce + where + T: std::borrow::Borrow, + I: IntoIterator, + { + let (r1s, r2s): (Vec, Vec) = nonces + .into_iter() + .map(|pubnonce| (pubnonce.borrow().R1, pubnonce.borrow().R2)) + .unzip(); + + AggNonce { + R1: Point::sum(r1s), + R2: Point::sum(r2s), + } + } + + /// Computes the nonce coefficient `b`, used to create the final nonce and signatures. + /// + /// Most use-cases will not need to invoke this method. Instead use + /// [`sign_solo`][crate::sign_solo] or [`sign_partial`][crate::sign_partial] + /// to create signatures. + pub fn nonce_coefficient( + &self, + aggregated_pubkey: impl Into, + message: impl AsRef<[u8]>, + ) -> S + where + S: From, + { + let hash: [u8; 32] = tagged_hashes::MUSIG_NONCECOEF_TAG_HASHER + .clone() + .chain_update(self.R1.serialize()) + .chain_update(self.R2.serialize()) + .chain_update(aggregated_pubkey.into().serialize_xonly()) + .chain_update(message.as_ref()) + .finalize() + .into(); + + S::from(MaybeScalar::reduce_from(&hash)) + } + + /// Computes the final public nonce point, published with the aggregated signature. + /// If this point winds up at infinity (probably due to a mischevious signer), we + /// instead return the generator point `G`. + /// + /// Most use-cases will not need to invoke this method. Instead use + /// [`sign_solo`][crate::sign_solo] or [`sign_partial`][crate::sign_partial] + /// to create signatures. + pub fn final_nonce

(&self, nonce_coeff: impl Into) -> P + where + P: From, + { + let nonce_coeff: MaybeScalar = nonce_coeff.into(); + let aggnonce_sum = self.R1 + (nonce_coeff * self.R2); + P::from(match aggnonce_sum { + MaybePoint::Infinity => Point::generator(), + MaybePoint::Valid(p) => p, + }) + } +} + +mod encodings { + use super::*; + + impl BinaryEncoding for SecNonce { + type Serialized = [u8; 64]; + + /// Returns the binary serialization of `SecNonce`, which serializes + /// both inner scalar values into a fixed-length 64-byte array. + /// + /// Note that this serialization differs from the format suggested + /// in BIP327, in that we do not include a public key. + fn to_bytes(&self) -> Self::Serialized { + let mut serialized = [0u8; 64]; + serialized[..32].clone_from_slice(&self.k1.serialize()); + serialized[32..].clone_from_slice(&self.k2.serialize()); + serialized + } + + /// Parses a `SecNonce` from a serialized byte slice. + /// This byte slice should be 64 bytes long, and encode two + /// non-zero 256-bit scalars. + /// + /// We also accept 97-byte long slices, to be compatible with BIP327's + /// suggested serialization format of `SecNonce`. + fn from_bytes(bytes: &[u8]) -> Result> { + if bytes.len() != 64 && bytes.len() != 97 { + return Err(DecodeError::bad_length(bytes.len())); + } + let k1 = Scalar::from_slice(&bytes[..32])?; + let k2 = Scalar::from_slice(&bytes[32..64])?; + Ok(SecNonce { k1, k2 }) + } + } + + impl BinaryEncoding for PubNonce { + type Serialized = [u8; 66]; + + /// Returns the binary serialization of `PubNonce`, which serializes + /// both inner points into a fixed-length 66-byte array. + fn to_bytes(&self) -> Self::Serialized { + let mut bytes = [0u8; 66]; + bytes[..33].clone_from_slice(&self.R1.serialize()); + bytes[33..].clone_from_slice(&self.R2.serialize()); + bytes + } + + /// Parses a `PubNonce` from a serialized byte slice. This byte slice should + /// be 66 bytes long, and encode two compressed, non-infinity curve points. + fn from_bytes(bytes: &[u8]) -> Result> { + if bytes.len() != 66 { + return Err(DecodeError::bad_length(bytes.len())); + } + let R1 = Point::from_slice(&bytes[..33])?; + let R2 = Point::from_slice(&bytes[33..])?; + Ok(PubNonce { R1, R2 }) + } + } + + impl BinaryEncoding for AggNonce { + type Serialized = [u8; 66]; + + /// Returns the binary serialization of `AggNonce`, which serializes + /// both inner points into a fixed-length 66-byte array. + fn to_bytes(&self) -> Self::Serialized { + let mut serialized = [0u8; 66]; + serialized[..33].clone_from_slice(&self.R1.serialize()); + serialized[33..].clone_from_slice(&self.R2.serialize()); + serialized + } + + /// Parses an `AggNonce` from a serialized byte slice. This byte slice should + /// be 66 bytes long, and encode two compressed (possibly infinity) curve points. + fn from_bytes(bytes: &[u8]) -> Result> { + if bytes.len() != 66 { + return Err(DecodeError::bad_length(bytes.len())); + } + let R1 = MaybePoint::from_slice(&bytes[..33])?; + let R2 = MaybePoint::from_slice(&bytes[33..])?; + Ok(AggNonce { R1, R2 }) + } + } + + impl_encoding_traits!(SecNonce, 64, 97); + impl_encoding_traits!(PubNonce, 66); + impl_encoding_traits!(AggNonce, 66); + + // Do not implement Display for SecNonce. + impl_hex_display!(PubNonce); + impl_hex_display!(AggNonce); +} + +impl

std::iter::Sum

for AggNonce +where + P: std::borrow::Borrow, +{ + /// Implements summation of partial public nonces into an aggregated nonce. + /// + /// ``` + /// use musig2::{AggNonce, PubNonce}; + /// + /// let nonces = [ + /// "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798\ + /// 032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE93" + /// .parse::() + /// .unwrap(), + /// "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61\ + /// 037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9" + /// .parse::() + /// .unwrap(), + /// ]; + /// + /// let expected = "02aebee092fe428c3b4c53993c3f80eecbf88ca935469b5bfcaabecb7b2afbb1a6\ + /// 03c923248ac1f639368bc82345698dfb445dca6024b9ba5a9bafe971bb5813964b" + /// .parse::() + /// .unwrap(); + /// + /// assert_eq!(nonces.iter().sum::(), expected); + /// assert_eq!(nonces.into_iter().sum::(), expected); + /// ``` + fn sum(iter: I) -> Self + where + I: Iterator, + { + let refs = iter.collect::>(); + AggNonce::sum(refs.iter().map(|nonce| nonce.borrow())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{testhex, KeyAggContext}; + + #[test] + fn test_nonce_generation() { + const NONCE_GEN_VECTORS: &[u8] = include_bytes!("test_vectors/nonce_gen_vectors.json"); + + #[derive(serde::Deserialize)] + struct NonceGenTestCase { + #[serde(rename = "rand", deserialize_with = "testhex::deserialize")] + nonce_seed: [u8; 32], + + #[serde(rename = "sk")] + seckey: Scalar, + + #[serde(rename = "aggpk", deserialize_with = "testhex::deserialize")] + aggregated_pubkey: [u8; 32], + + #[serde(rename = "msg", deserialize_with = "testhex::deserialize")] + message: Vec, + + #[serde(rename = "extra_in", deserialize_with = "testhex::deserialize")] + extra_input: Vec, + + expected_secnonce: SecNonce, + expected_pubnonce: PubNonce, + } + + #[derive(serde::Deserialize)] + struct NonceGenVectors { + test_cases: Vec, + } + + let vectors: NonceGenVectors = serde_json::from_slice(NONCE_GEN_VECTORS) + .expect("failed to parse test vectors from nonce_gen_vectors.json"); + + for test_case in vectors.test_cases { + let aggregated_pubkey = + Point::lift_x(&test_case.aggregated_pubkey).unwrap_or_else(|_| { + panic!( + "invalid aggregated xonly pubkey in test vector: {}", + base16ct::lower::encode_string(&test_case.aggregated_pubkey) + ) + }); + let secnonce = SecNonce::generate( + test_case.nonce_seed, + test_case.seckey, + aggregated_pubkey, + &test_case.message, + &test_case.extra_input, + ); + + assert_eq!(secnonce, test_case.expected_secnonce); + assert_eq!(secnonce.public_nonce(), test_case.expected_pubnonce); + } + } + + #[test] + fn test_nonce_aggregation() { + const NONCE_AGG_VECTORS: &[u8] = include_bytes!("test_vectors/nonce_agg_vectors.json"); + + #[derive(serde::Deserialize)] + struct NonceAggError { + signer: usize, + } + + #[derive(serde::Deserialize)] + struct NonceAggErrorTestCase { + #[serde(rename = "pnonce_indices")] + public_nonce_indexes: Vec, + error: NonceAggError, + } + + #[derive(serde::Deserialize)] + struct ValidNonceAggTestCase { + #[serde(rename = "pnonce_indices")] + public_nonce_indexes: Vec, + #[serde(rename = "expected")] + aggregated_nonce: AggNonce, + } + + #[derive(serde::Deserialize)] + struct NonceAggTestVectors { + #[serde(deserialize_with = "testhex::deserialize_vec", rename = "pnonces")] + public_nonces: Vec>, + + valid_test_cases: Vec, + error_test_cases: Vec, + } + + let vectors: NonceAggTestVectors = serde_json::from_slice(NONCE_AGG_VECTORS) + .expect("failed to parse test vectors from nonce_agg_vectors.json"); + + for test_case in vectors.valid_test_cases { + let nonces: Vec = test_case + .public_nonce_indexes + .into_iter() + .map(|i| { + PubNonce::from_bytes(&vectors.public_nonces[i]).unwrap_or_else(|_| { + panic!( + "used invalid nonce in valid test case: {}", + base16ct::lower::encode_string(&vectors.public_nonces[i]) + ) + }) + }) + .collect(); + + let aggregated_nonce = AggNonce::sum(&nonces); + + assert_eq!(aggregated_nonce, test_case.aggregated_nonce); + } + + for test_case in vectors.error_test_cases { + for (signer_index, i) in test_case.public_nonce_indexes.into_iter().enumerate() { + let nonce_result = PubNonce::try_from(vectors.public_nonces[i].as_slice()); + if signer_index == test_case.error.signer { + assert_eq!( + nonce_result, + Err(DecodeError::from(secp::errors::InvalidPointBytes)) + ); + } else { + nonce_result.unwrap_or_else(|_| { + panic!("unexpected pub nonce parsing error for signer {}", i) + }); + } + } + } + } + + #[test] + fn nonce_reuse_demo() { + let alice_seckey = Scalar::try_from([0x11; 32]).unwrap(); + let bob_seckey = Scalar::try_from([0x22; 32]).unwrap(); + + let alice_pubkey = alice_seckey * G; + let bob_pubkey = bob_seckey * G; + + let key_agg_ctx = KeyAggContext::new([alice_pubkey, bob_pubkey]).unwrap(); + + let message = b"you betta not sign this twice"; + + let alice_secnonce = SecNonceBuilder::new([0xAA; 32]).build(); + let bob_secnonce_1 = SecNonceBuilder::new([0xB1; 32]).build(); + let bob_secnonce_2 = SecNonceBuilder::new([0xB2; 32]).build(); + let bob_secnonce_3 = SecNonceBuilder::new([0xB3; 32]).build(); + + // First signature + let aggnonce_1 = + AggNonce::sum([alice_secnonce.public_nonce(), bob_secnonce_1.public_nonce()]); + let s1: MaybeScalar = crate::sign_partial( + &key_agg_ctx, + alice_seckey, + alice_secnonce.clone(), + &aggnonce_1, + message, + ) + .unwrap(); + + // Second signature + let aggnonce_2 = + AggNonce::sum([alice_secnonce.public_nonce(), bob_secnonce_2.public_nonce()]); + let s2: MaybeScalar = crate::sign_partial( + &key_agg_ctx, + alice_seckey, + alice_secnonce.clone(), + &aggnonce_2, + message, + ) + .unwrap(); + + // Third signature + let aggnonce_3 = + AggNonce::sum([alice_secnonce.public_nonce(), bob_secnonce_3.public_nonce()]); + let s3: MaybeScalar = crate::sign_partial( + &key_agg_ctx, + alice_seckey, + alice_secnonce.clone(), + &aggnonce_3, + message, + ) + .unwrap(); + + // Alice gives Bob `(s1, s2, s3)`. + // Bob can now compute Alice's secret key. + let a = key_agg_ctx.key_coefficient(alice_pubkey).unwrap(); + let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + + let b1: MaybeScalar = aggnonce_1.nonce_coefficient(aggregated_pubkey, message); + let b2: MaybeScalar = aggnonce_2.nonce_coefficient(aggregated_pubkey, message); + let b3: MaybeScalar = aggnonce_3.nonce_coefficient(aggregated_pubkey, message); + + let e1: MaybeScalar = crate::compute_challenge_hash_tweak( + &aggnonce_1.final_nonce::(b1).serialize_xonly(), + &key_agg_ctx.aggregated_pubkey(), + message, + ); + let e2: MaybeScalar = crate::compute_challenge_hash_tweak( + &aggnonce_2.final_nonce::(b2).serialize_xonly(), + &key_agg_ctx.aggregated_pubkey(), + message, + ); + let e3: MaybeScalar = crate::compute_challenge_hash_tweak( + &aggnonce_3.final_nonce::(b3).serialize_xonly(), + &key_agg_ctx.aggregated_pubkey(), + message, + ); + + let b2_diff = (b2 - b1).unwrap(); + let b3_diff = (b3 - b1).unwrap(); + + let top = (s3 - s1) * b2_diff - (s2 - s1) * b3_diff; + let bottom = a * ((e3 - e1) * b2_diff + (e1 - e2) * b3_diff); + let extracted_key = (top / bottom.unwrap()).unwrap(); + + assert_eq!(extracted_key, alice_seckey); + } +} diff --git a/crates/musig2/src/rkyv_wrappers.rs b/crates/musig2/src/rkyv_wrappers.rs new file mode 100644 index 00000000..d87056b7 --- /dev/null +++ b/crates/musig2/src/rkyv_wrappers.rs @@ -0,0 +1,190 @@ +use rkyv::{Archive, Deserialize, Serialize}; +use secp256k1::ffi::CPtr; + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Archive, Serialize, Deserialize)] +#[rkyv(remote = secp::Point)] +pub struct Point { + #[rkyv(getter = point_inner_getter, with = PublicKey)] + inner: secp256k1::PublicKey, +} + +fn point_inner_getter(p: &secp::Point) -> secp256k1::PublicKey { + p.clone().into() +} + +impl From for secp::Point { + fn from(value: Point) -> Self { + Self::from(value.inner) + } +} + +impl From for Point { + fn from(value: secp::Point) -> Self { + Self { + inner: value.into(), + } + } +} + +#[derive( + Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Archive, Serialize, Deserialize, +)] +#[rkyv(remote = secp256k1::PublicKey)] +pub struct PublicKey( + #[rkyv(getter = public_key_getter, with = FFIPublicKey)] secp256k1::ffi::PublicKey, +); + +fn public_key_getter(p: &secp256k1::PublicKey) -> secp256k1::ffi::PublicKey { + unsafe { *p.as_c_ptr().clone() } +} + +impl From for secp256k1::PublicKey { + fn from(value: PublicKey) -> Self { + value.0.into() + } +} + +impl From for PublicKey { + fn from(value: secp256k1::PublicKey) -> Self { + Self(public_key_getter(&value)) + } +} + +#[derive(Copy, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = secp256k1::ffi::PublicKey)] +pub struct FFIPublicKey(#[rkyv(getter = ffi_public_key_getter)] [u8; 64]); + +fn ffi_public_key_getter(p: &secp256k1::ffi::PublicKey) -> [u8; 64] { + p.underlying_bytes() +} + +impl From for secp256k1::ffi::PublicKey { + fn from(value: FFIPublicKey) -> Self { + unsafe { Self::from_array_unchecked(value.0) } + } +} + +impl From for FFIPublicKey { + fn from(value: secp256k1::ffi::PublicKey) -> Self { + Self(value.underlying_bytes()) + } +} + +#[derive(Copy, Clone, Debug, Archive, Serialize, Deserialize)] +#[rkyv(remote = subtle::Choice)] +pub struct Choice(#[rkyv(getter = choice_getter)] u8); + +fn choice_getter(c: &subtle::Choice) -> u8 { + c.unwrap_u8() +} + +impl From for subtle::Choice { + fn from(value: Choice) -> Self { + Self::from(value.0) + } +} + +impl From for Choice { + fn from(value: subtle::Choice) -> Self { + Self(value.unwrap_u8()) + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Archive, Serialize, Deserialize, +)] +#[rkyv(remote = secp::MaybePoint)] +pub enum MaybePoint { + Infinity, + Valid(#[rkyv(with = Point)] secp::Point), +} + +impl From for secp::MaybePoint { + fn from(value: MaybePoint) -> Self { + match value { + MaybePoint::Infinity => Self::Infinity, + MaybePoint::Valid(p) => Self::Valid(p), + } + } +} + +impl From for MaybePoint { + fn from(value: secp::MaybePoint) -> Self { + match value { + secp::MaybePoint::Infinity => Self::Infinity, + secp::MaybePoint::Valid(p) => Self::Valid(p.into()), + } + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Archive, Serialize, Deserialize)] +#[rkyv(remote = secp::MaybeScalar)] +pub enum MaybeScalar { + Zero, + Valid(#[rkyv(with = Scalar)] secp::Scalar), +} + +impl From for secp::MaybeScalar { + fn from(value: MaybeScalar) -> Self { + match value { + MaybeScalar::Zero => Self::Zero, + MaybeScalar::Valid(s) => Self::Valid(s), + } + } +} + +impl From for MaybeScalar { + fn from(value: secp::MaybeScalar) -> Self { + match value { + secp::MaybeScalar::Zero => Self::Zero, + secp::MaybeScalar::Valid(s) => Self::Valid(s), + } + } +} + +#[derive(Copy, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = secp::Scalar)] +pub struct Scalar { + #[rkyv(with = SecretKey, getter = scalar_getter)] + inner: secp256k1::SecretKey, +} + +fn scalar_getter(s: &secp::Scalar) -> secp256k1::SecretKey { + secp256k1::SecretKey::from_slice(&s.serialize()).unwrap() +} + +impl From for secp::Scalar { + fn from(value: Scalar) -> Self { + Self::from(value.inner) + } +} + +impl From for Scalar { + fn from(value: secp::Scalar) -> Self { + Self { + inner: value.into(), + } + } +} + +#[derive(Copy, Clone, Archive, Serialize, Deserialize)] +#[rkyv(remote = secp256k1::SecretKey)] +pub struct SecretKey( + #[rkyv(getter = secret_key_getter)] [u8; secp256k1::constants::SECRET_KEY_SIZE], +); + +fn secret_key_getter(sk: &secp256k1::SecretKey) -> [u8; secp256k1::constants::SECRET_KEY_SIZE] { + sk.secret_bytes() +} + +impl From for secp256k1::SecretKey { + fn from(value: SecretKey) -> Self { + Self::from_slice(&value.0).unwrap() + } +} + +impl From for SecretKey { + fn from(value: secp256k1::SecretKey) -> Self { + Self(value.secret_bytes()) + } +} diff --git a/crates/musig2/src/rounds.rs b/crates/musig2/src/rounds.rs new file mode 100644 index 00000000..95701684 --- /dev/null +++ b/crates/musig2/src/rounds.rs @@ -0,0 +1,724 @@ +use rkyv::{with::Map, Archive, Deserialize, Serialize}; +use secp::{MaybePoint, MaybeScalar, Point, Scalar}; + +use crate::{ + errors::{RoundContributionError, RoundFinalizeError, SignerIndexError, SigningError}, + rkyv_wrappers::{self}, + sign_partial, AdaptorSignature, AggNonce, KeyAggContext, LiftedSignature, NonceSeed, + PartialSignature, PubNonce, SecNonce, SecNonceSpices, +}; + +/// A simple state-machine which receives values of a given type `T` and +/// stores them in a vector at given indices. Returns an error if attempting +/// to fill a slot which is already taken by a different (not-equal) value. +#[derive(Archive, Serialize, Deserialize)] +pub struct Slots { + slots: Vec>, + open_slots: Vec, +} + +impl Slots { + /// Create a new set of slots. + fn new(expected_size: usize) -> Slots { + let mut slots = Vec::new(); + slots.resize(expected_size, None); + let open_slots = Vec::from_iter(0..expected_size); + Slots { slots, open_slots } + } + + /// Add an item to a specific slot, returning an error if the + /// slot is already taken by a different item. Idempotent. + fn place(&mut self, value: T, index: usize) -> Result<(), RoundContributionError> { + if index >= self.slots.len() { + return Err(RoundContributionError::out_of_range( + index, + self.slots.len(), + )); + } + + // Support idempotence. Callers can place the same value into the same + // slot index, which should be a no-op. + if let Some(ref existing) = self.slots[index] { + if &value == existing { + return Ok(()); + } else { + return Err(RoundContributionError::inconsistent_contribution(index)); + } + } + + self.slots[index] = Some(value); + self.open_slots + .remove(self.open_slots.binary_search(&index).unwrap()); + Ok(()) + } + + /// Returns a slice listing all remaining open slots. + fn remaining(&self) -> &[usize] { + self.open_slots.as_ref() + } + + /// Returns the full array of slot values in order. + /// Returns `None` if any slot is not yet filled. + fn finalize(self) -> Result, RoundFinalizeError> { + self.slots + .into_iter() + .map(|opt| opt.ok_or(RoundFinalizeError::Incomplete)) + .collect() + } +} + +#[derive(Archive, Serialize, Deserialize)] +#[rkyv(remote = Slots)] +struct PartialSignatureSlots { + #[rkyv(with = Map>)] + slots: Vec>, + open_slots: Vec, +} + +impl From for Slots { + fn from(partial_signature_slots: PartialSignatureSlots) -> Self { + let PartialSignatureSlots { slots, open_slots } = partial_signature_slots; + Slots { slots, open_slots } + } +} + +impl From> for PartialSignatureSlots { + fn from(partial_signature_slots: Slots) -> Self { + let Slots { slots, open_slots } = partial_signature_slots; + PartialSignatureSlots { slots, open_slots } + } +} + +/// A state machine which manages the first round of a MuSig2 signing session. +/// +/// Its task is to collect [`PubNonce`]s one by one until all signers have provided +/// one, at which point a partial signature can be created on a message using an +/// internally cached [`SecNonce`]. +/// +/// By preventing cloning or copying, and by consuming itself after creating a +/// partial signature, `FirstRound`'s API is written to encourage that a +/// [`SecNonce`] should **never be reused.** Take care not to shoot yourself in +/// the foot by attempting to work around this restriction. +#[derive(Archive, Serialize, Deserialize)] +pub struct FirstRound { + key_agg_ctx: KeyAggContext, + signer_index: usize, // Our key's index in `key_agg_ctx` + secnonce: SecNonce, // Our secret nonce. + pubnonce_slots: Slots, +} + +impl FirstRound { + /// Start the first round of a MuSig2 signing session. + /// + /// Generates the nonce using the given random seed value, which can + /// be any type that converts to `NonceSeed`. Usually this would + /// either be a `[u8; 32]` or any type that implements [`rand::RngCore`] + /// and [`rand::CryptoRng`], such as [`rand::rngs::OsRng`]. + /// If a static byte array is used as the seed, it should be generated + /// using a cryptographically secure RNG and discarded after the `FirstRound` + /// is created. Prefer using a [`rand::CryptoRng`] if possible, so that + /// there is no possibility of reusing the same nonce seed in a new signing + /// session. + /// + /// Returns an error if the given signer index is out of range. + pub fn new( + key_agg_ctx: KeyAggContext, + nonce_seed: impl Into, + signer_index: usize, + spices: SecNonceSpices<'_>, + ) -> Result { + let signer_pubkey: Point = key_agg_ctx + .get_pubkey(signer_index) + .ok_or_else(|| SignerIndexError::new(signer_index, key_agg_ctx.pubkeys().len()))?; + let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + + let secnonce = SecNonce::build(nonce_seed) + .with_pubkey(signer_pubkey) + .with_aggregated_pubkey(aggregated_pubkey) + .with_extra_input(&(signer_index as u32).to_be_bytes()) + .with_spices(spices) + .build(); + + let pubnonce = secnonce.public_nonce(); + + let mut pubnonce_slots = Slots::new(key_agg_ctx.pubkeys().len()); + pubnonce_slots.place(pubnonce, signer_index).unwrap(); // never fails + + Ok(FirstRound { + key_agg_ctx, + secnonce, + signer_index, + pubnonce_slots, + }) + } + + /// Returns the public nonce which should be shared with other signers. + pub fn our_public_nonce(&self) -> PubNonce { + self.secnonce.public_nonce() + } + + /// Returns a slice of all signer indexes who we have yet to receive a + /// [`PubNonce`] from. Note that since our nonce is generated and cached + /// internally, this slice will never contain the signer index provided to + /// [`FirstRound::new`] + pub fn holdouts(&self) -> &[usize] { + self.pubnonce_slots.remaining() + } + + /// Adds a [`PubNonce`] to the internal state, registering it to a specific + /// signer at a given index. Returns an error if the signer index is out + /// of range, or if we already have a different nonce on-file for that signer. + pub fn receive_nonce( + &mut self, + signer_index: usize, + pubnonce: PubNonce, + ) -> Result<(), RoundContributionError> { + self.pubnonce_slots.place(pubnonce, signer_index) + } + + /// Returns true once all public nonces have been received from every signer. + pub fn is_complete(&self) -> bool { + self.holdouts().is_empty() + } + + /// Finishes the first round once all nonces are received, combining nonces + /// into an aggregated nonce, and creating a partial signature using `seckey` + /// on a given `message`, both of which are stored in the returned `SecondRound`. + /// + /// See [`SecondRound::aggregated_nonce`] to access the aggregated nonce, + /// and [`SecondRound::our_signature`] to access the partial signature. + /// + /// This method intentionally consumes the `FirstRound`, to avoid accidentally + /// reusing a secret-nonce. + /// + /// This method should only be invoked once [`is_complete`][Self::is_complete] + /// returns true, otherwise it will fail. Can also return an error if partial + /// signing fails, probably because the wrong secret key was given. + /// + /// For all partial signatures to be valid, everyone must naturally be signing the + /// same message. + /// + /// This method is effectively the same as invoking + /// [`finalize_adaptor`][Self::finalize_adaptor], but passing [`MaybePoint::Infinity`] + /// as the adaptor point. + pub fn finalize( + self, + seckey: impl Into, + message: M, + ) -> Result, RoundFinalizeError> + where + M: AsRef<[u8]>, + { + self.finalize_adaptor(seckey, MaybePoint::Infinity, message) + } + + /// Finishes the first round once all nonces are received, combining nonces + /// into an aggregated nonce, and creating a partial adaptor signature using + /// `seckey` on a given `message`, both of which are stored in the returned + /// `SecondRound`. + /// + /// The `adaptor_point` is used to verifiably encrypt the partial signature, so that + /// the final aggregated signature will need to be adapted with the discrete log + /// of `adaptor_point` before the signature can be considered valid. All signers + /// must agree on and use the same adaptor point for the final signature to be valid. + /// + /// See [`SecondRound::aggregated_nonce`] to access the aggregated nonce, + /// and [`SecondRound::our_signature`] to access the partial signature. + /// + /// This method intentionally consumes the `FirstRound`, to avoid accidentally + /// reusing a secret-nonce. + /// + /// This method should only be invoked once [`is_complete`][Self::is_complete] + /// returns true, otherwise it will fail. Can also return an error if partial + /// signing fails, probably because the wrong secret key was given. + /// + /// For all partial signatures to be valid, everyone must naturally be signing the + /// same message. + pub fn finalize_adaptor( + self, + seckey: impl Into, + adaptor_point: impl Into, + message: M, + ) -> Result, RoundFinalizeError> + where + M: AsRef<[u8]>, + { + let adaptor_point: MaybePoint = adaptor_point.into(); + let pubnonces: Vec = self.pubnonce_slots.finalize()?; + let aggnonce = pubnonces.iter().sum(); + + let partial_signature = crate::adaptor::sign_partial( + &self.key_agg_ctx, + seckey, + self.secnonce, + &aggnonce, + adaptor_point, + &message, + )?; + + let mut partial_signature_slots = Slots::new(pubnonces.len()); + partial_signature_slots + .place(partial_signature, self.signer_index) + .unwrap(); // never fails + + let second_round = SecondRound { + key_agg_ctx: self.key_agg_ctx, + signer_index: self.signer_index, + pubnonces, + aggnonce, + adaptor_point, + message, + partial_signature_slots, + }; + + Ok(second_round) + } + + /// As an alternative to collecting nonces and partial signatures one-by-one from + /// everyone in the group, signers can opt instead to nominate an _aggregator node_ + /// whose duty is to collect nonces and signatures from all other signers, and + /// then broadcast the aggregated signature once they receive all partial signatures. + /// Doing this dramatically decreases the number of network round-trips required + /// for large groups of signers, and doesn't require any trust in the aggregator node + /// beyond the possibility that they may refuse to reveal the final signature. + /// + /// To use this API with a single aggregator node: + /// + /// - Instantiate the `FirstRound`. + /// - Send the output of [`FirstRound::our_public_nonce`] to the aggregator. + /// - The aggregator node should reply with an [`AggNonce`]. + /// - Once you receive the aggregated nonce, use [`FirstRound::sign_for_aggregator`] instead of + /// [`finalize`][Self::finalize] to consume the `FirstRound` and return a partial signature. + /// - Send this partial signature to the aggregator. + /// - The aggregator (if they are honest) will reply with the aggregated Schnorr signature, + /// which can be verified with [`verify_single`][crate::verify_single] + /// + /// [See the top-level crate documentation for an example](.#single-aggregator). + /// + /// Invoking this method is essentially the same as invoking + /// [`sign_for_aggregator_adaptor`][Self::sign_for_aggregator_adaptor], + /// but passing [`MaybePoint::Infinity`] as the adaptor point. + pub fn sign_for_aggregator( + self, + seckey: impl Into, + message: impl AsRef<[u8]>, + aggregated_nonce: &AggNonce, + ) -> Result + where + T: From, + { + sign_partial( + &self.key_agg_ctx, + seckey, + self.secnonce, + aggregated_nonce, + &message, + ) + } + + /// As an alternative to collecting nonces and partial signatures one-by-one from + /// everyone in the group, signers can opt instead to nominate an _aggregator node_ + /// whose duty is to collect nonces and signatures from all other signers, and + /// then broadcast the aggregated signature once they receive all partial signatures. + /// Doing this dramatically decreases the number of network round-trips required + /// for large groups of signers, and doesn't require any trust in the aggregator node + /// beyond the possibility that they may refuse to reveal the final signature. + /// + /// To use this API with a single aggregator node: + /// + /// - The group must agree on an `adaptor_point` which will be used to encrypt signatures. + /// - Instantiate the `FirstRound`. + /// - Send the output of [`FirstRound::our_public_nonce`] to the aggregator. + /// - The aggregator node should reply with an [`AggNonce`]. + /// - Once you receive the aggregated nonce, use [`FirstRound::sign_for_aggregator_adaptor`] + /// instead of [`finalize_adaptor`][Self::finalize_adaptor] to consume the `FirstRound` and + /// return a partial signature. + /// - Send this partial signature to the aggregator. + /// - The aggregator (if they are honest) will reply with the aggregated Schnorr signature, + /// which can be verified with [`adaptor::verify_single`][crate::adaptor::verify_single] + /// + /// [See the top-level crate documentation for an example](.#single-aggregator). + pub fn sign_for_aggregator_adaptor( + self, + seckey: impl Into, + adaptor_point: impl Into, + message: impl AsRef<[u8]>, + aggregated_nonce: &AggNonce, + ) -> Result + where + T: From, + { + crate::adaptor::sign_partial( + &self.key_agg_ctx, + seckey, + self.secnonce, + aggregated_nonce, + adaptor_point, + &message, + ) + } +} + +/// A state machine to manage second round of a MuSig2 signing session. +/// +/// This round handles collecting partial signatures one by one. Once +/// all signers have provided a signature, it can be finalized into +/// an aggregated Schnorr signature valid for the group's aggregated key. +#[derive(Archive, Serialize, Deserialize)] +pub struct SecondRound> { + key_agg_ctx: KeyAggContext, + signer_index: usize, + pubnonces: Vec, + aggnonce: AggNonce, + #[rkyv(with = rkyv_wrappers::MaybePoint)] + adaptor_point: MaybePoint, + message: M, + #[rkyv(with = PartialSignatureSlots)] + partial_signature_slots: Slots, +} + +impl> SecondRound { + /// Returns the aggregated nonce built from the nonces provided in the first round. + /// Signers who find themselves in an aggregator role can distribute this aggregated + /// nonce to other signers to that they can produce an aggregated signature without + /// 1:1 communication between every pair of signers. + pub fn aggregated_nonce(&self) -> &AggNonce { + &self.aggnonce + } + + /// Returns the partial signature created during finalization of the first round. + pub fn our_signature>(&self) -> T { + self.partial_signature_slots.slots[self.signer_index] + .map(T::from) + .unwrap() // never fails + } + + /// Returns a slice of all signer indexes from whom we have yet to receive a + /// [`PartialSignature`]. Note that since our signature was constructed + /// at the end of the first round, this slice will never contain the signer + /// index provided to [`FirstRound::new`]. + pub fn holdouts(&self) -> &[usize] { + self.partial_signature_slots.remaining() + } + + /// Adds a [`PartialSignature`] to the internal state, registering it to a specific + /// signer at a given index. Returns an error if the signature is not valid, or if + /// the given signer index is out of range, or if we already have a different partial + /// signature on-file for that signer. + pub fn receive_signature( + &mut self, + signer_index: usize, + partial_signature: impl Into, + ) -> Result<(), RoundContributionError> { + let partial_signature: PartialSignature = partial_signature.into(); + let signer_pubkey: Point = self.key_agg_ctx.get_pubkey(signer_index).ok_or_else(|| { + RoundContributionError::out_of_range(signer_index, self.key_agg_ctx.pubkeys().len()) + })?; + + crate::adaptor::verify_partial( + &self.key_agg_ctx, + partial_signature, + &self.aggnonce, + self.adaptor_point, + signer_pubkey, + &self.pubnonces[signer_index], + &self.message, + ) + .map_err(|_| RoundContributionError::invalid_signature(signer_index))?; + + self.partial_signature_slots + .place(partial_signature, signer_index)?; + + Ok(()) + } + + /// Returns true once we have all partial signatures from the group. + pub fn is_complete(&self) -> bool { + self.holdouts().is_empty() + } + + /// Finishes the second round once all partial signatures are received, + /// combining signatures into an aggregated signature on the `message` + /// given to [`FirstRound::finalize`]. + /// + /// This method should only be invoked once [`is_complete`][Self::is_complete] + /// returns true, otherwise it will fail. Can also return an error if partial + /// signature aggregation fails, but if [`receive_signature`][Self::receive_signature] + /// didn't complain, then finalizing will succeed with overwhelming probability. + /// + /// If the [`FirstRound`] was finalized with [`FirstRound::finalize_adaptor`], then + /// the second round must also be finalized with [`SecondRound::finalize_adaptor`], + /// otherwise this method will return [`RoundFinalizeError::InvalidAggregatedSignature`]. + pub fn finalize(self) -> Result + where + T: From, + { + let sig = self + .finalize_adaptor::()? + .adapt(MaybeScalar::Zero) + .expect("finalizing with empty adaptor should never result in an adaptor failure"); + + Ok(T::from(sig)) + } + + /// Finishes the second round once all partial adaptor signatures are received, + /// combining signatures into an aggregated adaptor signature on the `message` + /// given to [`FirstRound::finalize`]. + /// + /// To make this signature valid, it must then be adapted with the discrete log + /// of the adaptor point given to [`FirstRound::finalize`]. + /// + /// This method should only be invoked once [`is_complete`][Self::is_complete] + /// returns true, otherwise it will fail. Can also return an error if partial + /// signature aggregation fails, but if [`receive_signature`][Self::receive_signature] + /// didn't complain, then finalizing will succeed with overwhelming probability. + /// + /// If this signing session did not use adaptor signatures, the signature returned by + /// this method will be a valid signature which can be adapted with `MaybeScalar::Zero`. + pub fn finalize_adaptor(self) -> Result { + let partial_signatures: Vec = self.partial_signature_slots.finalize()?; + let final_signature = crate::adaptor::aggregate_partial_signatures( + &self.key_agg_ctx, + &self.aggnonce, + self.adaptor_point, + partial_signatures, + &self.message, + )?; + Ok(final_signature) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{verify_single, LiftedSignature}; + + #[test] + fn test_rounds_api() { + // SETUP phase: key aggregation + let seckeys = [ + "c52be0df73ef4354b2953deb9fdf77749b86946132176a33146f95d46fb065f3" + .parse::() + .unwrap(), + "c731a6d52303c68f3efc6c4262c99269140809c39f651196d7264d225c25360d" + .parse::() + .unwrap(), + "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" + .parse::() + .unwrap(), + ]; + + let pubkeys = seckeys.iter().map(|sk| sk.base_point_mul()); + let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + + // ROUND 1: nonces + + let message = "hello interwebz!"; + + let mut first_rounds: Vec = seckeys + .iter() + .enumerate() + .map(|(i, &sk)| { + FirstRound::new( + key_agg_ctx.clone(), + [0xAC; 32], + i, + SecNonceSpices::new().with_seckey(sk).with_message(&message), + ) + .unwrap_or_else(|_| { + panic!("failed to construct FirstRound machine for signer {}", i) + }) + }) + .collect(); + + // Nobody's round should be complete right after it was created. + for (i, round) in first_rounds.iter().enumerate() { + assert!( + !round.is_complete(), + "round should not be complete without any nonces" + ); + + let mut expected_holdouts: Vec = (0..seckeys.len()).collect(); + expected_holdouts.remove(i); + assert_eq!( + round.holdouts(), + expected_holdouts, + "expected holdouts list to contain all other signers" + ) + } + + let pubnonces: Vec = first_rounds + .iter() + .map(|first_round| first_round.our_public_nonce()) + .collect(); + + // Distribute the pubnonces. + for (i, nonce) in pubnonces.iter().enumerate() { + for round in first_rounds.iter_mut() { + round + .receive_nonce(i, nonce.clone()) + .unwrap_or_else(|_| panic!("should receive nonce {} OK", i)); + + let mut expected_holdouts: Vec = (0..seckeys.len()).collect(); + expected_holdouts.retain(|&j| j != round.signer_index && j > i); + assert_eq!(round.holdouts(), expected_holdouts); + + // Confirm the round completes only once all nonces are received + if expected_holdouts.is_empty() { + assert!( + round.is_complete(), + "first round should have completed after signer {} receiving nonce {}", + round.signer_index, + i + ); + } else { + assert!( + !round.is_complete(), + "first round should not have completed after signer {} receiving nonce {}", + round.signer_index, + i + ); + } + } + } + + // The first round of nonce sharing should be complete now. + for round in first_rounds.iter() { + assert!(round.is_complete()); + } + + assert_eq!( + first_rounds[0].receive_nonce(2, pubnonces[1].clone()), + Err(RoundContributionError::inconsistent_contribution(2)), + "receiving a different nonce at a previously used index should fail" + ); + assert_eq!( + first_rounds[0].receive_nonce(pubnonces.len() + 1, pubnonces[1].clone()), + Err(RoundContributionError::out_of_range( + pubnonces.len() + 1, + pubnonces.len() + )), + "receiving a nonce at an invalid index should fail" + ); + + // ROUND 2: signing + + let mut second_rounds: Vec> = first_rounds + .into_iter() + .enumerate() + .map(|(i, first_round)| -> SecondRound<&str> { + first_round + .finalize(seckeys[i], message) + .unwrap_or_else(|_| panic!("failed to finalize first round for signer {}", i)) + }) + .collect(); + + for round in second_rounds.iter() { + assert!( + !round.is_complete(), + "second round should not be complete yet" + ); + } + + // Invalid partial signatures should be automatically rejected. + { + let wrong_nonce = SecNonce::build([0xCC; 32]).build(); + let invalid_partial_signature: PartialSignature = sign_partial( + &key_agg_ctx, + seckeys[0], + wrong_nonce, + &second_rounds[0].aggnonce, + message, + ) + .unwrap(); + + assert_eq!( + second_rounds[1].receive_signature(0, invalid_partial_signature), + Err(RoundContributionError::invalid_signature(0)), + "partial signature with invalid nonce should be rejected" + ); + } + + let partial_signatures: Vec = second_rounds + .iter() + .map(|round| round.our_signature()) + .collect(); + + // Distribute the partial signatures. + for (i, &partial_signature) in partial_signatures.iter().enumerate() { + for (receiver_index, round) in second_rounds.iter_mut().enumerate() { + round + .receive_signature(i, partial_signature) + .unwrap_or_else(|_| panic!("should receive partial signature {} OK", i)); + + let mut expected_holdouts: Vec = (0..seckeys.len()).collect(); + expected_holdouts.retain(|&j| j != receiver_index && j > i); + assert_eq!(round.holdouts(), expected_holdouts); + + // Confirm the round completes only once all signatures are received + if expected_holdouts.is_empty() { + assert!( + round.is_complete(), + "second round should have completed after signer {} receiving partial signature {}", + receiver_index, + i + ); + } else { + assert!( + !round.is_complete(), + "second round should not have completed after signer {} receiving partial signature {}", + receiver_index, + i + ); + } + } + } + + // The second round should be complete now that everyone has each + // other's partial signatures. + for round in second_rounds.iter() { + assert!(round.is_complete()); + } + + // Test supplying signatures at wrong indices + assert_eq!( + second_rounds[0].receive_signature(2, partial_signatures[1]), + Err(RoundContributionError::invalid_signature(2)), + "receiving a valid partial signature for the wrong signer should fail" + ); + assert_eq!( + second_rounds[0].receive_signature(partial_signatures.len() + 1, partial_signatures[1]), + Err(RoundContributionError::out_of_range( + partial_signatures.len() + 1, + partial_signatures.len() + )), + "receiving a partial signature at an invalid index should fail" + ); + + // FINALIZATION: signatures can now be aggregated. + let mut signatures: Vec = second_rounds + .into_iter() + .enumerate() + .map(|(i, round)| { + round + .finalize() + .unwrap_or_else(|_| panic!("failed to finalize second round for signer {}", i)) + }) + .collect(); + + let last_sig = signatures.pop().unwrap(); + + // All signers should output the same aggregated signature. + for sig in signatures { + assert_eq!( + sig, last_sig, + "some signers created different aggregated signatures" + ); + } + + // and of course, the sig should be verifiable as a standard schnorr signature. + let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); + verify_single(aggregated_pubkey, last_sig, message) + .expect("aggregated signature should be valid"); + } +} diff --git a/crates/musig2/src/sig_agg.rs b/crates/musig2/src/sig_agg.rs new file mode 100644 index 00000000..1d4d1462 --- /dev/null +++ b/crates/musig2/src/sig_agg.rs @@ -0,0 +1,283 @@ +use secp::{MaybePoint, MaybeScalar, Point, G}; + +use crate::errors::VerifyError; +use crate::{ + compute_challenge_hash_tweak, AdaptorSignature, AggNonce, KeyAggContext, LiftedSignature, + PartialSignature, +}; + +/// Aggregate a collection of partial adaptor signatures together into a final +/// adaptor signature on a given `message`, under the aggregated public key in +/// `key_agg_ctx`. +/// +/// The resulting signature will not be valid unless adapted with the discrete log +/// of the `adaptor_point`. +/// +/// Returns an error if the resulting signature would not be valid. +pub fn aggregate_partial_adaptor_signatures>( + key_agg_ctx: &KeyAggContext, + aggregated_nonce: &AggNonce, + adaptor_point: impl Into, + partial_signatures: impl IntoIterator, + message: impl AsRef<[u8]>, +) -> Result { + let adaptor_point: MaybePoint = adaptor_point.into(); + let aggregated_pubkey = key_agg_ctx.pubkey; + + let b: MaybeScalar = aggregated_nonce.nonce_coefficient(aggregated_pubkey, &message); + let final_nonce: Point = aggregated_nonce.final_nonce(b); + let adapted_nonce = final_nonce + adaptor_point; + let nonce_x_bytes = adapted_nonce.serialize_xonly(); + let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &aggregated_pubkey, &message); + + let aggregated_signature = partial_signatures + .into_iter() + .map(|sig| sig.into()) + .sum::() + + (e * key_agg_ctx.tweak_acc).negate_if(aggregated_pubkey.parity()); + + let effective_nonce = if adapted_nonce.has_even_y() { + final_nonce + } else { + -final_nonce + }; + + // Ensure the signature will verify as valid. + if aggregated_signature * G != effective_nonce + e * aggregated_pubkey.to_even_y() { + return Err(VerifyError::BadSignature); + } + + let adaptor_sig = AdaptorSignature { + R: MaybePoint::Valid(final_nonce), + s: aggregated_signature, + }; + Ok(adaptor_sig) +} + +/// Aggregate a collection of partial signatures together into a final +/// signature on a given `message`, valid under the aggregated public +/// key in `key_agg_ctx`. +/// +/// Returns an error if the resulting signature would not be valid. +pub fn aggregate_partial_signatures( + key_agg_ctx: &KeyAggContext, + aggregated_nonce: &AggNonce, + partial_signatures: impl IntoIterator, + message: impl AsRef<[u8]>, +) -> Result +where + S: Into, + T: From, +{ + let sig = aggregate_partial_adaptor_signatures( + key_agg_ctx, + aggregated_nonce, + MaybePoint::Infinity, + partial_signatures, + message, + )? + .adapt(MaybeScalar::Zero) + .map(T::from) + .expect("aggregating with empty adaptor should never result in an adaptor failure"); + + Ok(sig) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testhex; + use crate::{verify_single, CompactSignature, PubNonce, SecNonce}; + + use secp::{Point, Scalar}; + + #[test] + fn test_aggregate_partial_signatures() { + const SIG_AGG_VECTORS: &[u8] = include_bytes!("test_vectors/sig_agg_vectors.json"); + + #[derive(serde::Deserialize)] + struct ValidSigAggTestCase { + #[serde(rename = "aggnonce")] + aggregated_nonce: AggNonce, + nonce_indices: Vec, + key_indices: Vec, + tweak_indices: Vec, + is_xonly: Vec, + psig_indices: Vec, + + #[serde(rename = "expected")] + aggregated_signature: CompactSignature, + } + + #[derive(serde::Deserialize)] + struct SigAggVectors { + pubkeys: Vec, + + #[serde(rename = "pnonces")] + public_nonces: Vec, + + tweaks: Vec, + + #[serde(rename = "psigs", deserialize_with = "testhex::deserialize_vec")] + partial_signatures: Vec>, + + #[serde(rename = "msg", deserialize_with = "testhex::deserialize")] + message: Vec, + + valid_test_cases: Vec, + } + + let vectors: SigAggVectors = serde_json::from_slice(SIG_AGG_VECTORS) + .expect("failed to parse test vectors from sig_agg_vectors.json"); + + for test_case in vectors.valid_test_cases { + let pubkeys = test_case + .key_indices + .into_iter() + .map(|i| vectors.pubkeys[i]); + + let public_nonces = test_case + .nonce_indices + .into_iter() + .map(|i| &vectors.public_nonces[i]); + + let aggregated_nonce = AggNonce::sum(public_nonces); + + assert_eq!( + &aggregated_nonce, &test_case.aggregated_nonce, + "aggregated nonce does not match test vector" + ); + + let mut key_agg_ctx = + KeyAggContext::new(pubkeys).expect("error constructing key aggregation context"); + + key_agg_ctx = test_case + .tweak_indices + .into_iter() + .map(|i| vectors.tweaks[i]) + .zip(test_case.is_xonly) + .fold(key_agg_ctx, |ctx, (tweak, is_xonly)| { + ctx.with_tweak(tweak, is_xonly).unwrap_or_else(|_| { + panic!("failed to tweak key agg context with {:x}", tweak) + }) + }); + + let partial_signatures: Vec = test_case + .psig_indices + .into_iter() + .map(|i| { + Scalar::try_from(vectors.partial_signatures[i].as_slice()) + .expect("failed to parse partial signature") + }) + .collect(); + + let aggregated_signature: CompactSignature = aggregate_partial_signatures( + &key_agg_ctx, + &aggregated_nonce, + partial_signatures, + &vectors.message, + ) + .expect("failed to aggregate partial signatures"); + + assert_eq!( + &aggregated_signature, &test_case.aggregated_signature, + "incorrect aggregated signature" + ); + + verify_single(key_agg_ctx.pubkey, aggregated_signature, &vectors.message) + .unwrap_or_else(|_| { + panic!( + "aggregated signature {} should be valid BIP340 signature", + aggregated_signature + ) + }); + } + } + + #[test] + fn test_adaptor_signature_aggregation() { + const ITERATIONS: usize = 10; + + for _ in 0..ITERATIONS { + let seckeys = [ + Scalar::random(&mut rand::thread_rng()), + Scalar::random(&mut rand::thread_rng()), + Scalar::random(&mut rand::thread_rng()), + ]; + + let pubkeys = [ + seckeys[0].base_point_mul(), + seckeys[1].base_point_mul(), + seckeys[2].base_point_mul(), + ]; + + let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + + let message = b"danger, will robinson!"; + + let secnonces = [ + SecNonce::random(&mut rand::thread_rng()), + SecNonce::random(&mut rand::thread_rng()), + SecNonce::random(&mut rand::thread_rng()), + ]; + + let pubnonces = [ + secnonces[0].public_nonce(), + secnonces[1].public_nonce(), + secnonces[2].public_nonce(), + ]; + + let aggnonce = AggNonce::sum(&pubnonces); + + let adaptor_secret = Scalar::random(&mut rand::thread_rng()); + let adaptor_point = adaptor_secret.base_point_mul(); + + let partial_signatures: Vec = seckeys + .into_iter() + .zip(secnonces) + .map(|(seckey, secnonce)| { + crate::adaptor::sign_partial( + &key_agg_ctx, + seckey, + secnonce, + &aggnonce, + adaptor_point, + message, + ) + }) + .collect::, _>>() + .expect("failed to create partial adaptor signatures"); + + let adaptor_signature: AdaptorSignature = crate::adaptor::aggregate_partial_signatures( + &key_agg_ctx, + &aggnonce, + adaptor_point, + partial_signatures.iter().copied(), + message, + ) + .expect("failed to aggregate partial adaptor signatures"); + + crate::adaptor::verify_single( + key_agg_ctx.aggregated_pubkey::(), + &adaptor_signature, + message, + adaptor_point, + ) + .expect("invalid aggregated adaptor signature"); + + let valid_signature = adaptor_signature.adapt(adaptor_secret).unwrap(); + verify_single( + key_agg_ctx.aggregated_pubkey::(), + valid_signature, + message, + ) + .expect("invalid decrypted adaptor signature"); + + let revealed: MaybeScalar = adaptor_signature + .reveal_secret(&valid_signature) + .expect("should compute adaptor secret from decrypted signature"); + + assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); + } + } +} diff --git a/crates/musig2/src/signature.rs b/crates/musig2/src/signature.rs new file mode 100644 index 00000000..075b0fcc --- /dev/null +++ b/crates/musig2/src/signature.rs @@ -0,0 +1,432 @@ +use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; + +use crate::errors::DecodeError; +use crate::BinaryEncoding; + +/// The number of bytes in a binary-serialized Schnorr signature. +pub const SCHNORR_SIGNATURE_SIZE: usize = 64; + +/// Represents a compacted Schnorr signature, either +/// from an aggregated signing session or a single signer. +/// +/// It differs from [`LiftedSignature`] in that a `CompactSignature` +/// contains the X-only serialized coordinate of the signature's nonce +/// point `R`, whereas a [`LiftedSignature`] contains the parsed curve +/// point `R`. +/// +/// Parsing a curve point from a byte array requires some computations which +/// can be optimized away during verification. This is why `CompactSignature` +/// is its own separate type. +/// +/// Rules for when to use each signature type during verification: +/// +/// - Prefer using [`CompactSignature`] when parsing and verifying single +/// signatures. That will produce faster results as you won't need to +/// lift the X-only coordinate of the nonce-point to verify the signature. +/// - Prefer using [`LiftedSignature`] when using batch verification, +/// because lifted signatures are required for batch verification +/// so you might as well keep the signatures in lifted form. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CompactSignature { + /// The X-only byte representation of the public nonce point `R`. + pub rx: [u8; 32], + + /// The signature scalar which proves knowledge of the secret key and nonce. + pub s: MaybeScalar, +} + +impl CompactSignature { + /// Constructs a `CompactSignature` from a signature pair `(R, s)`. + pub fn new(R: impl Into, s: impl Into) -> CompactSignature { + CompactSignature { + rx: R.into().serialize_xonly(), + s: s.into(), + } + } + + /// Lifts the nonce point X coordinate to a proper point with even parity, + /// returning an error if the coordinate was not on the curve. + pub fn lift_nonce(&self) -> Result { + let R = Point::lift_x(&self.rx)?; + Ok(LiftedSignature { R, s: self.s }) + } +} + +/// A representation of a Schnorr signature point+scalar pair `(R, s)`. +/// +/// Differs from [`CompactSignature`] in that a `LiftedSignature` +/// contains the full nonce point `R`, which is parsed as a valid +/// curve point. +/// +/// Rules for when to use each signature type during verification: +/// +/// - Prefer using [`CompactSignature`] when parsing and verifying single +/// signatures. That will produce faster results as you won't need to +/// lift the X-only coordinate of the nonce-point to verify the signature. +/// - Prefer using [`LiftedSignature`] when using batch verification, +/// because lifted signatures are required for batch verification +/// so you might as well keep the signatures in lifted form. +/// +/// A `LiftedSignature` has the exact sime binary serialization +/// format as a [`CompactSignature`], because the Y-coordinate +/// of the nonce point is implicit - It is always assumed to be +/// the even-parity point. +/// +/// To construct a `LiftedSignature`, use [`LiftedSignature::new`] +/// to ensure the Y-coordinate of the nonce point is always converted +/// to even-parity. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LiftedSignature { + pub(crate) R: Point, + pub(crate) s: MaybeScalar, +} + +impl LiftedSignature { + /// Constructs a new lifted signature by converting the nonce point `R` + /// to even parity. + /// + /// Accepts any types which convert to a [`secp::Point`] and + /// [`secp::MaybeScalar`]. + pub fn new(R: impl Into, s: impl Into) -> LiftedSignature { + LiftedSignature { + R: R.into().to_even_y(), + s: s.into(), + } + } + + /// Compact the finalized signature by serializing the + /// nonce point as an X-coordinate-only byte array. + pub fn compact(&self) -> CompactSignature { + CompactSignature::new(self.R, self.s) + } + + /// Encrypts an existing valid signature by subtracting a given adaptor secret. + pub fn encrypt(&self, adaptor_secret: impl Into) -> AdaptorSignature { + AdaptorSignature::new(self.R, self.s).encrypt(adaptor_secret) + } + + /// Unzip this signature pair into a tuple of any two types + /// which convert from [`secp::Point`] and [`secp::MaybeScalar`]. + /// + /// ``` + /// // This allows us to use `R` as a variable name. + /// #![allow(non_snake_case)] + /// + /// let signature = "c1de0db357c5d780c69624d0ab266a3b6866301adc85b66cc9fce26d2a60b72c\ + /// 659c15ed9bc81df681e1e0607cf44cc08e77396f74359de1e6e6ff365ca94dae" + /// .parse::() + /// .unwrap(); + /// + /// let (R, s): ([u8; 33], [u8; 32]) = signature.unzip(); + /// let (R, s): (secp::Point, secp::MaybeScalar) = signature.unzip(); + /// # #[cfg(feature = "k256")] + /// # { + /// let (R, s): (k256::PublicKey, k256::Scalar) = signature.unzip(); + /// # } + /// # #[cfg(feature = "secp256k1")] + /// # { + /// let (R, s): (secp256k1::PublicKey, secp::MaybeScalar) = signature.unzip(); + /// # } + /// ``` + pub fn unzip(&self) -> (P, S) + where + P: From, + S: From, + { + (P::from(self.R), S::from(self.s)) + } +} + +/// A representation of a Schnorr adaptor signature point+scalar pair `(R', s')`. +/// +/// Differs from [`LiftedSignature`] in that an `AdaptorSignature` is explicitly +/// modified with by specific scalar offset called the _adaptor secret,_ so that +/// only by learning the adaptor secret can its holder convert it +/// into a valid BIP340 signature. +/// +/// Since `AdaptorSignature` is not meant for on-chain consensus, the nonce +/// point `R` can have either even or odd parity, and so `AdaptorSignature` +/// is encoded as a 65 byte array which includes the compressed `R` point. +/// +/// To learn more about adaptor signatures and how to use them, see the docs +/// in [the adaptor module][crate::adaptor]. +/// +/// To construct an `AdaptorSignature`, use [`LiftedSignature::encrypt`], +/// [`adaptor::sign_solo`][crate::adaptor::sign_solo], or +/// [`adaptor::aggregate_partial_signatures`][crate::adaptor::aggregate_partial_signatures]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AdaptorSignature { + pub(crate) R: MaybePoint, + pub(crate) s: MaybeScalar, +} + +impl AdaptorSignature { + /// Constructs a new adaptor signature from a nonce and scalar pair. + /// + /// Accepts any types which convert to a [`secp::MaybePoint`] and + /// [`secp::MaybeScalar`]. + pub fn new(R: impl Into, s: impl Into) -> AdaptorSignature { + AdaptorSignature { + R: R.into(), + s: s.into(), + } + } + + /// Adapts the signature into a lifted signature with a given adaptor secret. + /// + /// Returns `None` if the nonce resulting from adding the adaptor point is the + /// point at infinity. + /// + /// The resulting signature is not guaranteed to be valid unless the + ///`AdaptorSignature` was already verified with + /// [`adaptor::verify_single`][crate::adaptor::verify_single]. + /// If not, make sure to verify the resulting lifted signature + /// using [`verify_single`][crate::verify_single]. + pub fn adapt>( + &self, + adaptor_secret: impl Into, + ) -> Option { + let adaptor_secret: MaybeScalar = adaptor_secret.into(); + let adapted_nonce = (self.R + adaptor_secret * G).into_option()?; + let adapted_sig = self.s + adaptor_secret.negate_if(adapted_nonce.parity()); + Some(T::from(LiftedSignature::new(adapted_nonce, adapted_sig))) + } + + /// Encrypts an existing adaptor signature again, by subtracting another adaptor secret. + pub fn encrypt(&self, adaptor_secret: impl Into) -> AdaptorSignature { + let adaptor_secret: Scalar = adaptor_secret.into(); + AdaptorSignature { + R: self.R - adaptor_secret * G, + s: self.s - adaptor_secret, + } + } + + /// Using a decrypted signature `final_sig`, this method computes the + /// adaptor secret used to encrypt this signature. + /// + /// Returns `None` if `final_sig` is not related to this adaptor signature. + pub fn reveal_secret(&self, final_sig: &LiftedSignature) -> Option + where + S: From, + { + let t = final_sig.s - self.s; + let T = t * G; + + if T == final_sig.R - self.R { + Some(S::from(t)) + } else if T == final_sig.R + self.R { + Some(S::from(-t)) + } else { + None + } + } + + /// Unzip this signature pair into a tuple of any two types + /// which convert from [`secp::MaybePoint`] and [`secp::MaybeScalar`]. + /// + /// ``` + /// // This allows us to use `R` as a variable name. + /// #![allow(non_snake_case)] + /// + /// let signature = "02c1de0db357c5d780c69624d0ab266a3b6866301adc85b66cc9fce26d2a60b72c\ + /// 659c15ed9bc81df681e1e0607cf44cc08e77396f74359de1e6e6ff365ca94dae" + /// .parse::() + /// .unwrap(); + /// + /// let (R, s): ([u8; 33], [u8; 32]) = signature.unzip(); + /// let (R, s): (secp::MaybePoint, secp::MaybeScalar) = signature.unzip(); + /// # #[cfg(feature = "k256")] + /// # { + /// let (R, s): (k256::AffinePoint, k256::Scalar) = signature.unzip(); + /// # } + /// ``` + pub fn unzip(&self) -> (P, S) + where + P: From, + S: From, + { + (P::from(self.R), S::from(self.s)) + } +} + +mod encodings { + use super::*; + + impl BinaryEncoding for CompactSignature { + type Serialized = [u8; SCHNORR_SIGNATURE_SIZE]; + + /// Serializes the signature to a compact 64-byte encoding, + /// including the X coordinate of the `R` point and the + /// serialized `s` scalar. + fn to_bytes(&self) -> Self::Serialized { + let mut serialized = [0u8; SCHNORR_SIGNATURE_SIZE]; + serialized[..32].clone_from_slice(&self.rx); + serialized[32..].clone_from_slice(&self.s.serialize()); + serialized + } + + /// Deserialize a compact Schnorr signature from a byte slice. This + /// slice must be exactly [`SCHNORR_SIGNATURE_SIZE`] bytes long. + fn from_bytes(signature_bytes: &[u8]) -> Result> { + if signature_bytes.len() != SCHNORR_SIGNATURE_SIZE { + return Err(DecodeError::bad_length(signature_bytes.len())); + } + let rx = <[u8; 32]>::try_from(&signature_bytes[..32]).unwrap(); + let s = MaybeScalar::try_from(&signature_bytes[32..])?; + Ok(CompactSignature { rx, s }) + } + } + + impl BinaryEncoding for LiftedSignature { + type Serialized = [u8; SCHNORR_SIGNATURE_SIZE]; + + /// Serializes the signature to a compact 64-byte encoding, + /// including the X coordinate of the `R` point and the + /// serialized `s` scalar. + fn to_bytes(&self) -> Self::Serialized { + CompactSignature::from(*self).to_bytes() + } + + /// Deserialize a compact Schnorr signature from a byte slice. This + /// slice must be exactly [`SCHNORR_SIGNATURE_SIZE`] bytes long. + fn from_bytes(bytes: &[u8]) -> Result> { + let compact_signature = CompactSignature::from_bytes(bytes).map_err(|e| e.convert())?; + Ok(compact_signature.lift_nonce()?) + } + } + + impl BinaryEncoding for AdaptorSignature { + type Serialized = [u8; 65]; + + /// Serializes the signature to a compressed 65-byte encoding, + /// including the compressed `R` point and the serialized `s` scalar. + fn to_bytes(&self) -> Self::Serialized { + let mut serialized = [0u8; 65]; + serialized[..33].clone_from_slice(&self.R.serialize()); + serialized[33..].clone_from_slice(&self.s.serialize()); + serialized + } + + /// Deserialize an adaptor signature from a byte slice. This + /// slice must be exactly 65 bytes long. + fn from_bytes(signature_bytes: &[u8]) -> Result> { + if signature_bytes.len() != 65 { + return Err(DecodeError::bad_length(signature_bytes.len())); + } + let R = MaybePoint::try_from(&signature_bytes[..33])?; + let s = MaybeScalar::try_from(&signature_bytes[33..])?; + Ok(AdaptorSignature { R, s }) + } + } + + impl_encoding_traits!(CompactSignature, SCHNORR_SIGNATURE_SIZE); + impl_encoding_traits!(LiftedSignature, SCHNORR_SIGNATURE_SIZE); + impl_encoding_traits!(AdaptorSignature, 65); + + impl_hex_display!(CompactSignature); + impl_hex_display!(LiftedSignature); + impl_hex_display!(AdaptorSignature); +} + +mod internal_conversions { + use super::*; + + impl TryFrom for LiftedSignature { + type Error = secp::errors::InvalidPointBytes; + + /// Convert the compact signature into an `(R, s)` pair by lifting + /// the nonce point's X-coordinate representation. Fails if the + /// X-coordinate bytes do not represent a valid curve point. + fn try_from(signature: CompactSignature) -> Result { + signature.lift_nonce() + } + } + + impl From for CompactSignature { + /// Converts a pair `(R, s)` into a schnorr signature struct. + fn from(signature: LiftedSignature) -> Self { + signature.compact() + } + } +} + +#[cfg(feature = "secp256k1")] +mod secp256k1_conversions { + use super::*; + + impl TryFrom for CompactSignature { + type Error = DecodeError; + fn try_from(signature: secp256k1::schnorr::Signature) -> Result { + Self::try_from(signature.serialize()) + } + } + + impl TryFrom for LiftedSignature { + type Error = DecodeError; + fn try_from(signature: secp256k1::schnorr::Signature) -> Result { + Self::try_from(signature.serialize()) + } + } + + impl From for secp256k1::schnorr::Signature { + fn from(signature: CompactSignature) -> Self { + Self::from_slice(&signature.to_bytes()).unwrap() // Never fails + } + } + + impl From for secp256k1::schnorr::Signature { + fn from(signature: LiftedSignature) -> Self { + Self::from_slice(&signature.to_bytes()).unwrap() // Never fails + } + } +} + +#[cfg(feature = "k256")] +mod k256_conversions { + use super::*; + + impl From<(k256::PublicKey, k256::Scalar)> for CompactSignature { + fn from((R, s): (k256::PublicKey, k256::Scalar)) -> Self { + CompactSignature::new(R, s) + } + } + + impl From<(k256::PublicKey, k256::Scalar)> for LiftedSignature { + fn from((R, s): (k256::PublicKey, k256::Scalar)) -> Self { + LiftedSignature::new(R, s) + } + } + + impl TryFrom for (k256::PublicKey, k256::Scalar) { + type Error = secp::errors::InvalidPointBytes; + fn try_from(signature: CompactSignature) -> Result { + Ok(signature.lift_nonce()?.unzip()) + } + } + + impl From for (k256::PublicKey, k256::Scalar) { + fn from(signature: LiftedSignature) -> Self { + signature.unzip() + } + } + + impl From for (k256::AffinePoint, k256::Scalar) { + fn from(signature: LiftedSignature) -> Self { + signature.unzip() + } + } + + #[cfg(feature = "k256")] + impl From for k256::WideBytes { + fn from(signature: CompactSignature) -> Self { + <[u8; SCHNORR_SIGNATURE_SIZE]>::from(signature).into() + } + } + + #[cfg(feature = "k256")] + impl From for k256::WideBytes { + fn from(signature: LiftedSignature) -> Self { + <[u8; SCHNORR_SIGNATURE_SIZE]>::from(signature).into() + } + } +} diff --git a/crates/musig2/src/signing.rs b/crates/musig2/src/signing.rs new file mode 100644 index 00000000..60ed94a4 --- /dev/null +++ b/crates/musig2/src/signing.rs @@ -0,0 +1,615 @@ +use crate::errors::{SigningError, VerifyError}; +use crate::{tagged_hashes, AggNonce, KeyAggContext, PubNonce, SecNonce}; + +use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; + +use sha2::Digest as _; + +/// Partial signatures are just scalars in the range `[0, n)`. +/// +/// See the documentation of [`secp::MaybeScalar`] for the +/// parsing, serializing, and conversion traits available +/// on this type. +pub type PartialSignature = MaybeScalar; + +/// Computes the challenge hash `e` for for a signature. You probably don't need +/// to call this directly. Instead use [`sign_solo`][crate::sign_solo] or +/// [`sign_partial`][crate::sign_partial]. +pub fn compute_challenge_hash_tweak>( + final_nonce_xonly: &[u8; 32], + aggregated_pubkey: &Point, + message: impl AsRef<[u8]>, +) -> S { + let hash: [u8; 32] = tagged_hashes::BIP0340_CHALLENGE_TAG_HASHER + .clone() + .chain_update(final_nonce_xonly) + .chain_update(aggregated_pubkey.serialize_xonly()) + .chain_update(message.as_ref()) + .finalize() + .into(); + + S::from(MaybeScalar::reduce_from(&hash)) +} + +/// Compute a partial signature on a message encrypted under an adaptor point. +/// +/// The partial signature returned from this function is a potentially-zero +/// scalar value which can then be passed to other signers for verification +/// and aggregation. +/// +/// Once aggregated, the signature must be adapted with the discrete log +/// (secret key) of `adaptor_point` for the signature to be considered valid. +/// +/// Returns an error if the given secret key does not belong to this +/// `key_agg_ctx`. As an added safety, we also verify the partial signature +/// before returning it. +pub fn sign_partial_adaptor>( + key_agg_ctx: &KeyAggContext, + seckey: impl Into, + secnonce: SecNonce, + aggregated_nonce: &AggNonce, + adaptor_point: impl Into, + message: impl AsRef<[u8]>, +) -> Result { + let adaptor_point: MaybePoint = adaptor_point.into(); + let seckey: Scalar = seckey.into(); + let pubkey = seckey.base_point_mul(); + + // As a side-effect, looking up the cached key coefficient also confirms + // the individual key is indeed part of the aggregated key. + let key_coeff = key_agg_ctx + .key_coefficient(pubkey) + .ok_or(SigningError::UnknownKey)?; + + let aggregated_pubkey = key_agg_ctx.pubkey; + let pubnonce = secnonce.public_nonce(); + + let b: MaybeScalar = aggregated_nonce.nonce_coefficient(aggregated_pubkey, &message); + let final_nonce: Point = aggregated_nonce.final_nonce(b); + let adapted_nonce = final_nonce + adaptor_point; + + // `d` is negated if only one of the parity accumulator OR the aggregated pubkey + // has odd parity. + let d = seckey.negate_if(aggregated_pubkey.parity() ^ key_agg_ctx.parity_acc); + + let nonce_x_bytes = adapted_nonce.serialize_xonly(); + let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &aggregated_pubkey, &message); + + // if has_even_Y(R): + // k = k1 + b*k2 + // else: + // k = (n-k1) + b(n-k2) + // = n - (k1 + b*k2) + let secnonce_sum = (secnonce.k1 + b * secnonce.k2).negate_if(adapted_nonce.parity()); + + // s = k + e*a*d + let partial_signature = secnonce_sum + (e * key_coeff * d); + + verify_partial_adaptor( + key_agg_ctx, + partial_signature, + aggregated_nonce, + adaptor_point, + pubkey, + &pubnonce, + &message, + )?; + + Ok(T::from(partial_signature)) +} + +/// Compute a partial signature on a message. +/// +/// The partial signature returned from this function is a potentially-zero +/// scalar value which can then be passed to other signers for verification +/// and aggregation. +/// +/// Returns an error if the given secret key does not belong to this +/// `key_agg_ctx`. As an added safety, we also verify the partial signature +/// before returning it. +/// +/// This is equivalent to invoking [`sign_partial_adaptor`], but passing +/// [`MaybePoint::Infinity`] as the adaptor point. +pub fn sign_partial>( + key_agg_ctx: &KeyAggContext, + seckey: impl Into, + secnonce: SecNonce, + aggregated_nonce: &AggNonce, + message: impl AsRef<[u8]>, +) -> Result { + sign_partial_adaptor( + key_agg_ctx, + seckey, + secnonce, + aggregated_nonce, + MaybePoint::Infinity, + message, + ) +} + +/// Verify a partial signature, usually from an untrusted co-signer, +/// which has been encrypted under an adaptor point. +/// +/// If `verify_partial_adaptor` succeeds for every signature in +/// a signing session, the resulting aggregated signature is guaranteed +/// to be valid once it is adapted with the discrete log (secret key) +/// of `adaptor_point`. +/// +/// Returns an error if the given public key doesn't belong to the +/// `key_agg_ctx`, or if the signature is invalid. +pub fn verify_partial_adaptor( + key_agg_ctx: &KeyAggContext, + partial_signature: impl Into, + aggregated_nonce: &AggNonce, + adaptor_point: impl Into, + individual_pubkey: impl Into, + individual_pubnonce: &PubNonce, + message: impl AsRef<[u8]>, +) -> Result<(), VerifyError> { + let partial_signature: MaybeScalar = partial_signature.into(); + + // As a side-effect, looking up the cached effective key also confirms + // the individual key is indeed part of the aggregated key. + let effective_pubkey: MaybePoint = key_agg_ctx + .effective_pubkey(individual_pubkey) + .ok_or(VerifyError::UnknownKey)?; + + let aggregated_pubkey = key_agg_ctx.pubkey; + + let b: MaybeScalar = aggregated_nonce.nonce_coefficient(aggregated_pubkey, &message); + let final_nonce: Point = aggregated_nonce.final_nonce(b); + let adapted_nonce = final_nonce + adaptor_point.into(); + + let mut effective_nonce = individual_pubnonce.R1 + b * individual_pubnonce.R2; + + // Don't need constant time ops here as adapted_nonce is public. + if adapted_nonce.has_odd_y() { + effective_nonce = -effective_nonce; + } + + let nonce_x_bytes = adapted_nonce.serialize_xonly(); + let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &aggregated_pubkey, &message); + + // s * G == R + (g * gacc * e * a * P) + let challenge_parity = aggregated_pubkey.parity() ^ key_agg_ctx.parity_acc; + let challenge_point = (e * effective_pubkey).negate_if(challenge_parity); + + if partial_signature * G != effective_nonce + challenge_point { + return Err(VerifyError::BadSignature); + } + + Ok(()) +} + +/// Verify a partial signature, usually from an untrusted co-signer. +/// +/// If `verify_partial` succeeds for every signature in +/// a signing session, the resulting aggregated signature is guaranteed +/// to be valid. +/// +/// This function is effectively the same as invoking [`verify_partial_adaptor`] +/// but passing [`MaybePoint::Infinity`] as the adaptor point. +/// +/// Returns an error if the given public key doesn't belong to the +/// `key_agg_ctx`, or if the signature is invalid. +pub fn verify_partial( + key_agg_ctx: &KeyAggContext, + partial_signature: impl Into, + aggregated_nonce: &AggNonce, + individual_pubkey: impl Into, + individual_pubnonce: &PubNonce, + message: impl AsRef<[u8]>, +) -> Result<(), VerifyError> { + verify_partial_adaptor( + key_agg_ctx, + partial_signature, + aggregated_nonce, + MaybePoint::Infinity, + individual_pubkey, + individual_pubnonce, + message, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::errors::DecodeError; + use crate::testhex; + + #[test] + fn test_partial_sign_and_verify() { + const SIGN_VERIFY_VECTORS: &[u8] = include_bytes!("test_vectors/sign_verify_vectors.json"); + + #[derive(serde::Deserialize)] + struct ValidSignVerifyTestCase { + key_indices: Vec, + nonce_indices: Vec, + aggnonce_index: usize, + msg_index: usize, + signer_index: usize, + expected: MaybeScalar, + } + + #[derive(serde::Deserialize, Clone)] + struct SignError { + signer: Option, + } + + #[derive(serde::Deserialize, Clone)] + struct SignErrorTestCase { + key_indices: Vec, + aggnonce_index: usize, + msg_index: usize, + secnonce_index: usize, + error: SignError, + comment: String, + } + + #[derive(serde::Deserialize)] + struct VerifyFailTestCase { + #[serde(rename = "sig", deserialize_with = "testhex::deserialize")] + partial_signature: Vec, + key_indices: Vec, + nonce_indices: Vec, + msg_index: usize, + signer_index: usize, + comment: String, + } + + #[derive(serde::Deserialize)] + struct SignVerifyVectors { + #[serde(rename = "sk")] + seckey: Scalar, + + #[serde(deserialize_with = "testhex::deserialize_vec")] + pubkeys: Vec<[u8; 33]>, + + #[serde(rename = "secnonces", deserialize_with = "testhex::deserialize_vec")] + secret_nonces: Vec<[u8; 97]>, + + #[serde(rename = "pnonces", deserialize_with = "testhex::deserialize_vec")] + public_nonces: Vec<[u8; 66]>, + + #[serde(rename = "aggnonces", deserialize_with = "testhex::deserialize_vec")] + aggregated_nonces: Vec<[u8; 66]>, + + #[serde(rename = "msgs", deserialize_with = "testhex::deserialize_vec")] + messages: Vec>, + + valid_test_cases: Vec, + sign_error_test_cases: Vec, + verify_fail_test_cases: Vec, + } + + let vectors: SignVerifyVectors = serde_json::from_slice(SIGN_VERIFY_VECTORS) + .expect("error parsing test vectors from sign_verify.json"); + + let secnonce = SecNonce::try_from(vectors.secret_nonces[0].as_ref()) + .expect("error parsing secret nonce"); + + for (test_index, test_case) in vectors.valid_test_cases.into_iter().enumerate() { + let pubkeys: Vec = test_case + .key_indices + .into_iter() + .map(|i| { + Point::try_from(&vectors.pubkeys[i]).unwrap_or_else(|_| { + panic!( + "invalid pubkey used in valid test: {}", + base16ct::lower::encode_string(&vectors.pubkeys[i]) + ) + }) + }) + .collect(); + + let signer_pubkey = pubkeys[test_case.signer_index]; + assert_eq!(signer_pubkey, vectors.seckey.base_point_mul()); + + let aggnonce_bytes = &vectors.aggregated_nonces[test_case.aggnonce_index]; + let aggregated_nonce = AggNonce::from_bytes(aggnonce_bytes).unwrap_or_else(|_| { + panic!( + "invalid aggregated nonce used in valid test case: {}", + base16ct::lower::encode_string(aggnonce_bytes) + ) + }); + + let key_agg_ctx = + KeyAggContext::new(pubkeys).expect("error constructing key aggregation context"); + + let message = &vectors.messages[test_case.msg_index]; + + let partial_signature: PartialSignature = sign_partial( + &key_agg_ctx, + vectors.seckey, + secnonce.clone(), + &aggregated_nonce, + message, + ) + .expect("error during partial signing"); + + assert_eq!( + partial_signature, test_case.expected, + "partial signature does not match expected for test case {}", + test_index, + ); + + let adaptor_secret = MaybeScalar::Valid(vectors.seckey); + let adaptor_point = adaptor_secret * G; + let partial_adaptor_signature: PartialSignature = sign_partial_adaptor( + &key_agg_ctx, + vectors.seckey, + secnonce.clone(), + &aggregated_nonce, + adaptor_point, + message, + ) + .expect("error during partial adaptor signing"); + + let public_nonces: Vec = test_case + .nonce_indices + .into_iter() + .map(|i| { + PubNonce::from_bytes(&vectors.public_nonces[i]).unwrap_or_else(|_| { + panic!( + "invalid pubnonce in valid test: {}", + base16ct::lower::encode_string(&vectors.public_nonces[i]) + ) + }) + }) + .collect(); + + // Ensure the aggregated nonce in the test vector is correct + assert_eq!(&AggNonce::sum(&public_nonces), &aggregated_nonce); + + verify_partial( + &key_agg_ctx, + partial_signature, + &aggregated_nonce, + signer_pubkey, + &public_nonces[test_case.signer_index], + message, + ) + .expect("failed to verify valid partial signature"); + + verify_partial_adaptor( + &key_agg_ctx, + partial_adaptor_signature, + &aggregated_nonce, + adaptor_point, + signer_pubkey, + &public_nonces[test_case.signer_index], + message, + ) + .expect("failed to verify valid partial signature"); + } + + // invalid input test case 0: signer's pubkey is not in the key_agg_ctx + { + let test_case = vectors.sign_error_test_cases[0].clone(); + + let pubkeys: Vec = test_case + .key_indices + .into_iter() + .map(|i| Point::try_from(&vectors.pubkeys[i]).unwrap()) + .collect(); + + let aggnonce_bytes = &vectors.aggregated_nonces[test_case.aggnonce_index]; + let aggregated_nonce = AggNonce::from_bytes(aggnonce_bytes).unwrap(); + + let key_agg_ctx = + KeyAggContext::new(pubkeys).expect("error constructing key aggregation context"); + + let message = &vectors.messages[test_case.msg_index]; + + assert_eq!( + sign_partial::( + &key_agg_ctx, + vectors.seckey, + secnonce.clone(), + &aggregated_nonce, + message, + ), + Err(SigningError::UnknownKey), + "partial signing should fail for pubkey not in key_agg_ctx", + ); + } + + // invalid input test case 1: invalid pubkey + { + let test_case = &vectors.sign_error_test_cases[1]; + for (signer_index, &key_index) in test_case.key_indices.iter().enumerate() { + let result = Point::try_from(&vectors.pubkeys[key_index]); + if signer_index == test_case.error.signer.unwrap() { + assert_eq!( + result, + Err(secp::errors::InvalidPointBytes), + "expected invalid signer pubkey" + ); + } else { + result.expect("expected valid signer pubkey"); + } + } + } + + // invalid input test case 2, 3, and 4: invalid aggnonce + { + for test_case in vectors.sign_error_test_cases[2..5].iter() { + let result = + AggNonce::from_bytes(&vectors.aggregated_nonces[test_case.aggnonce_index]); + + assert_eq!( + result, + Err(DecodeError::from(secp::errors::InvalidPointBytes)), + "{} - invalid AggNonce should fail to decode", + &test_case.comment + ); + } + } + + // invalid input test case 5: invalid secnonce + { + let test_case = &vectors.sign_error_test_cases[5]; + let result = SecNonce::from_bytes(&vectors.secret_nonces[test_case.secnonce_index]); + assert_eq!( + result, + Err(DecodeError::from(secp::errors::InvalidScalarBytes)), + "invalid SecNonce should fail to decode" + ); + } + + // Verification failure test cases 0 and 1: fake signatures + { + for test_case in vectors.verify_fail_test_cases[..2].iter() { + let pubkeys: Vec = test_case + .key_indices + .iter() + .map(|&i| Point::try_from(&vectors.pubkeys[i]).unwrap()) + .collect(); + + let signer_pubkey = pubkeys[test_case.signer_index]; + + let key_agg_ctx = KeyAggContext::new(pubkeys) + .expect("error constructing key aggregation context"); + + let public_nonces: Vec = test_case + .nonce_indices + .iter() + .map(|&i| PubNonce::from_bytes(&vectors.public_nonces[i]).unwrap()) + .collect(); + + let aggregated_nonce = AggNonce::sum(&public_nonces); + + let message = &vectors.messages[test_case.msg_index]; + + let partial_signature = + MaybeScalar::try_from(test_case.partial_signature.as_slice()) + .expect("unexpected invalid partial signature"); + + assert_eq!( + verify_partial( + &key_agg_ctx, + partial_signature, + &aggregated_nonce, + signer_pubkey, + &public_nonces[test_case.signer_index], + message, + ), + Err(VerifyError::BadSignature), + "{} - unexpected success while verifying invalid partial signature", + test_case.comment, + ); + } + } + + // Verification failure test case 2: invalid signature + { + let test_case = &vectors.verify_fail_test_cases[2]; + let result = PartialSignature::try_from(test_case.partial_signature.as_slice()); + assert_eq!( + result, + Err(secp::errors::InvalidScalarBytes), + "unexpected valid partial signature" + ); + } + } + + #[test] + fn test_sign_with_tweaks() { + const TWEAK_VECTORS: &[u8] = include_bytes!("test_vectors/tweak_vectors.json"); + + #[derive(serde::Deserialize)] + struct ValidTweakTestCase { + key_indices: Vec, + nonce_indices: Vec, + tweak_indices: Vec, + is_xonly: Vec, + signer_index: usize, + #[serde(rename = "expected")] + partial_signature: MaybeScalar, + } + + #[derive(serde::Deserialize)] + struct TweakVectors { + #[serde(rename = "sk")] + seckey: Scalar, + pubkeys: Vec, + + #[serde(rename = "secnonce")] + secret_nonces: SecNonce, + + #[serde(rename = "pnonces")] + public_nonces: Vec, + + #[serde(rename = "aggnonce")] + aggregated_nonce: AggNonce, + + #[serde(deserialize_with = "testhex::deserialize_vec")] + tweaks: Vec>, + + #[serde(rename = "msg", deserialize_with = "testhex::deserialize")] + message: Vec, + + valid_test_cases: Vec, + } + + let vectors: TweakVectors = + serde_json::from_slice(TWEAK_VECTORS).expect("failed to parse test_vectors/tweak.json"); + + for test_case in vectors.valid_test_cases { + let pubkeys: Vec = test_case + .key_indices + .into_iter() + .map(|i| vectors.pubkeys[i]) + .collect(); + + let signer_pubkey = pubkeys[test_case.signer_index]; + + let mut key_agg_ctx = + KeyAggContext::new(pubkeys).expect("error creating key aggregation context"); + + key_agg_ctx = test_case + .tweak_indices + .into_iter() + .map(|i| { + Scalar::try_from(vectors.tweaks[i].as_slice()) + .expect("failed to parse valid tweak value") + }) + .zip(test_case.is_xonly) + .fold(key_agg_ctx, |ctx, (tweak, is_xonly)| { + ctx.with_tweak(tweak, is_xonly).unwrap_or_else(|_| { + panic!("failed to tweak key agg context with {:x}", tweak) + }) + }); + + let partial_signature: PartialSignature = sign_partial( + &key_agg_ctx, + vectors.seckey, + vectors.secret_nonces.clone(), + &vectors.aggregated_nonce, + &vectors.message, + ) + .expect("error during partial signing"); + + assert_eq!( + partial_signature, test_case.partial_signature, + "incorrect tweaked partial signature", + ); + + let public_nonces: Vec<&PubNonce> = test_case + .nonce_indices + .into_iter() + .map(|i| &vectors.public_nonces[i]) + .collect(); + + verify_partial( + &key_agg_ctx, + partial_signature, + &vectors.aggregated_nonce, + signer_pubkey, + public_nonces[test_case.signer_index], + &vectors.message, + ) + .expect("failed to verify valid partial signature"); + } + } +} diff --git a/crates/musig2/src/tagged_hashes.rs b/crates/musig2/src/tagged_hashes.rs new file mode 100644 index 00000000..6d30b399 --- /dev/null +++ b/crates/musig2/src/tagged_hashes.rs @@ -0,0 +1,208 @@ +//! This module holds declarations for computing +//! [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)-style +//! tagged hashes. +//! +//! A tagged hash is a SHA256 hash which has been prefixed with two copies of +//! the SHA256 hash of a given fixed constant byte string. This has the effect +//! of namespacing the hash to reduce the possibility of collisions. +//! +//! You probably won't need to use these hashes yourself, but if you want to +//! produce a tagged hash, simply clone one of the lazily allocated hash engines +//! declared as statics in this module. This will give you an instance of +//! `sha2::Sha256`. +//! +//! ``` +//! use musig2::tagged_hashes; +//! use sha2::Sha256; +//! use sha2::Digest as _; // Brings trait methods into scope +//! +//! let hash = tagged_hashes::KEYAGG_LIST_TAG_HASHER +//! .clone() +//! .chain_update(b"SomeData") +//! .finalize(); +//! +//! let expected = { +//! let tag_digest = Sha256::digest("KeyAgg list"); +//! Sha256::new() +//! .chain_update(&tag_digest) +//! .chain_update(&tag_digest) +//! .chain_update(b"SomeData") +//! .finalize() +//! }; +//! +//! assert_eq!(hash, expected); +//! ``` + +use once_cell::sync::Lazy; +use sha2::Sha256; + +use sha2::Digest as _; + +fn with_tag_hash_prefix(tag_hash: [u8; 32]) -> Sha256 { + Sha256::new().chain_update(tag_hash).chain_update(tag_hash) +} + +/// sha256(b"KeyAgg list") +const KEYAGG_LIST_TAG_DIGEST: [u8; 32] = [ + 0x48, 0x1C, 0x97, 0x1C, 0x3C, 0x0B, 0x46, 0xD7, 0xF0, 0xB2, 0x75, 0xAE, 0x59, 0x8D, 0x4E, 0x2C, + 0x7E, 0xD7, 0x31, 0x9C, 0x59, 0x4A, 0x5C, 0x6E, 0xC7, 0x9E, 0xA0, 0xD4, 0x99, 0x02, 0x94, 0xF0, +]; + +/// sha256(b"KeyAgg coefficient") +const KEYAGG_COEFF_TAG_DIGEST: [u8; 32] = [ + 0xBF, 0xC9, 0x04, 0x03, 0x4D, 0x1C, 0x88, 0xE8, 0xC8, 0x0E, 0x22, 0xE5, 0x3D, 0x24, 0x56, 0x6D, + 0x64, 0x82, 0x4E, 0xD6, 0x42, 0x72, 0x81, 0xC0, 0x91, 0x00, 0xF9, 0x4D, 0xCD, 0x52, 0xC9, 0x81, +]; + +/// sha256(b"MuSig/aux") +const MUSIG_AUX_TAG_DIGEST: [u8; 32] = [ + 0x40, 0x8F, 0x8C, 0x1F, 0x29, 0x24, 0x21, 0xB5, 0x56, 0x9E, 0xBC, 0x6C, 0xB5, 0xF2, 0xE2, 0x0C, + 0xF1, 0xE3, 0x84, 0x1B, 0x47, 0x43, 0x9F, 0xCC, 0x58, 0x7D, 0x20, 0xE3, 0xC1, 0x7F, 0x08, 0x37, +]; + +/// sha256(b"MuSig/nonce") +const MUSIG_NONCE_TAG_DIGEST: [u8; 32] = [ + 0xF8, 0xC1, 0x0C, 0xBC, 0x61, 0x4E, 0xD1, 0xA0, 0x84, 0xB4, 0x37, 0x05, 0x2B, 0x5D, 0x2C, 0x4B, + 0x50, 0x1A, 0x9D, 0xE7, 0xAA, 0xFB, 0xE3, 0x48, 0xAC, 0xE8, 0x02, 0x6C, 0xA7, 0xFC, 0xB1, 0x7B, +]; + +/// sha256(b"MuSig/noncecoef") +const MUSIG_NONCECOEF_TAG_DIGEST: [u8; 32] = [ + 0x5A, 0x6D, 0x45, 0xF6, 0xDA, 0x29, 0xE6, 0x51, 0xCB, 0x1B, 0xA2, 0xB8, 0xAC, 0x2C, 0xDD, 0x4E, + 0xBC, 0x15, 0xC2, 0xFB, 0xB2, 0x89, 0xF0, 0xCC, 0x82, 0x1B, 0xBF, 0x0A, 0x34, 0x09, 0x5F, 0x32, +]; + +/// sha256(b"BIP0340/aux") +const BIP0340_AUX_TAG_DIGEST: [u8; 32] = [ + 0xF1, 0xEF, 0x4E, 0x5E, 0xC0, 0x63, 0xCA, 0xDA, 0x6D, 0x94, 0xCA, 0xFA, 0x9D, 0x98, 0x7E, 0xA0, + 0x69, 0x26, 0x58, 0x39, 0xEC, 0xC1, 0x1F, 0x97, 0x2D, 0x77, 0xA5, 0x2E, 0xD8, 0xC1, 0xCC, 0x90, +]; + +/// sha256(b"BIP0340/nonce") +const BIP0340_NONCE_TAG_DIGEST: [u8; 32] = [ + 0x07, 0x49, 0x77, 0x34, 0xA7, 0x9B, 0xCB, 0x35, 0x5B, 0x9B, 0x8C, 0x7D, 0x03, 0x4F, 0x12, 0x1C, + 0xF4, 0x34, 0xD7, 0x3E, 0xF7, 0x2D, 0xDA, 0x19, 0x87, 0x00, 0x61, 0xFB, 0x52, 0xBF, 0xEB, 0x2F, +]; + +/// sha256(b"BIP0340/challenge") +const BIP0340_CHALLENGE_TAG_DIGEST: [u8; 32] = [ + 0x7B, 0xB5, 0x2D, 0x7A, 0x9F, 0xEF, 0x58, 0x32, 0x3E, 0xB1, 0xBF, 0x7A, 0x40, 0x7D, 0xB3, 0x82, + 0xD2, 0xF3, 0xF2, 0xD8, 0x1B, 0xB1, 0x22, 0x4F, 0x49, 0xFE, 0x51, 0x8F, 0x6D, 0x48, 0xD3, 0x7C, +]; + +/// sha256(b"BIP0340/batch") +const BIP0340_BATCH_TAG_DIGEST: [u8; 32] = [ + 0x77, 0x06, 0x39, 0x59, 0x84, 0x1F, 0xFA, 0x7B, 0x06, 0x15, 0x4E, 0xE0, 0x47, 0x50, 0x19, 0x40, + 0x36, 0x48, 0x7A, 0xB8, 0x91, 0x96, 0xD0, 0x6E, 0xC7, 0x3E, 0x75, 0x82, 0x90, 0x98, 0x41, 0xB5, +]; + +/// sha256(b"TapTweak") +const TAPROOT_TWEAK_TAG_DIGEST: [u8; 32] = [ + 0xe8, 0x0f, 0xe1, 0x63, 0x9c, 0x9c, 0xa0, 0x50, 0xe3, 0xaf, 0x1b, 0x39, 0xc1, 0x43, 0xc6, 0x3e, + 0x42, 0x9c, 0xbc, 0xeb, 0x15, 0xd9, 0x40, 0xfb, 0xb5, 0xc5, 0xa1, 0xf4, 0xaf, 0x57, 0xc5, 0xe9, +]; + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"KeyAgg list") || sha256(b"KeyAgg list") +/// ``` +pub static KEYAGG_LIST_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(KEYAGG_LIST_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"KeyAgg coefficient") || sha256(b"KeyAgg coefficient") +/// ``` +pub static KEYAGG_COEFF_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(KEYAGG_COEFF_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"MuSig/aux") || sha256(b"MuSig/aux") +/// ``` +pub static MUSIG_AUX_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(MUSIG_AUX_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"MuSig/nonce") || sha256(b"MuSig/nonce") +/// ``` +pub static MUSIG_NONCE_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(MUSIG_NONCE_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"MuSig/noncecoef") || sha256(b"MuSig/noncecoef") +/// ``` +pub static MUSIG_NONCECOEF_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(MUSIG_NONCECOEF_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"BIP0340/aux") || sha256(b"BIP0340/aux") +/// ``` +pub static BIP0340_AUX_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(BIP0340_AUX_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"BIP0340/nonce") || sha256(b"BIP0340/nonce") +/// ``` +pub static BIP0340_NONCE_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(BIP0340_NONCE_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"BIP0340/challenge") || sha256(b"BIP0340/challenge") +/// ``` +pub static BIP0340_CHALLENGE_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(BIP0340_CHALLENGE_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"BIP0340/batch") || sha256(b"BIP0340/batch") +/// ``` +pub static BIP0340_BATCH_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(BIP0340_BATCH_TAG_DIGEST)); + +/// A `sha2::Sha256` hash engine with its state initialized to: +/// +/// ```notrust +/// sha256(b"TapTweak") || sha256(b"TapTweak") +/// ``` +pub static TAPROOT_TWEAK_TAG_HASHER: Lazy = + Lazy::new(|| with_tag_hash_prefix(TAPROOT_TWEAK_TAG_DIGEST)); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tagged_hash() { + let test_cases = [ + ("KeyAgg list", KEYAGG_LIST_TAG_DIGEST), + ("KeyAgg coefficient", KEYAGG_COEFF_TAG_DIGEST), + ("MuSig/aux", MUSIG_AUX_TAG_DIGEST), + ("MuSig/nonce", MUSIG_NONCE_TAG_DIGEST), + ("MuSig/noncecoef", MUSIG_NONCECOEF_TAG_DIGEST), + ("BIP0340/aux", BIP0340_AUX_TAG_DIGEST), + ("BIP0340/nonce", BIP0340_NONCE_TAG_DIGEST), + ("BIP0340/challenge", BIP0340_CHALLENGE_TAG_DIGEST), + ("BIP0340/batch", BIP0340_BATCH_TAG_DIGEST), // custom + ("TapTweak", TAPROOT_TWEAK_TAG_DIGEST), + ]; + for (tag, declared_hash) in test_cases { + let actual_hash = <[u8; 32]>::from(sha2::Sha256::digest(tag)); + assert_eq!(declared_hash, actual_hash); + } + } +} diff --git a/crates/musig2/src/test_vectors/bip340_vectors.csv b/crates/musig2/src/test_vectors/bip340_vectors.csv new file mode 100644 index 00000000..aa317a3b --- /dev/null +++ b/crates/musig2/src/test_vectors/bip340_vectors.csv @@ -0,0 +1,20 @@ +index,secret key,public key,aux_rand,message,signature,verification result,comment +0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, +1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, +2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, +3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n +4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, +5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve +6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false +7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message +8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value +9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 +10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 +11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve +12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size +13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order +14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size +15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) +16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) +17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) +18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/crates/musig2/src/test_vectors/key_agg_vectors.json b/crates/musig2/src/test_vectors/key_agg_vectors.json new file mode 100644 index 00000000..b2e623de --- /dev/null +++ b/crates/musig2/src/test_vectors/key_agg_vectors.json @@ -0,0 +1,88 @@ +{ + "pubkeys": [ + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "020000000000000000000000000000000000000000000000000000000000000005", + "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", + "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" + ], + "tweaks": [ + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B" + ], + "valid_test_cases": [ + { + "key_indices": [0, 1, 2], + "expected": "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C" + }, + { + "key_indices": [2, 1, 0], + "expected": "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B" + }, + { + "key_indices": [0, 0, 0], + "expected": "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935" + }, + { + "key_indices": [0, 0, 1, 1], + "expected": "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E" + } + ], + "error_test_cases": [ + { + "key_indices": [0, 3], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubkey" + }, + "comment": "Invalid public key" + }, + { + "key_indices": [0, 4], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubkey" + }, + "comment": "Public key exceeds field size" + }, + { + "key_indices": [5, 0], + "tweak_indices": [], + "is_xonly": [], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubkey" + }, + "comment": "First byte of public key is not 2 or 3" + }, + { + "key_indices": [0, 1], + "tweak_indices": [0], + "is_xonly": [true], + "error": { + "type": "value", + "message": "The tweak must be less than n." + }, + "comment": "Tweak is out of range" + }, + { + "key_indices": [6], + "tweak_indices": [1], + "is_xonly": [false], + "error": { + "type": "value", + "message": "The result of tweaking cannot be infinity." + }, + "comment": "Intermediate tweaking result is point at infinity" + } + ] +} diff --git a/crates/musig2/src/test_vectors/key_sort_vectors.json b/crates/musig2/src/test_vectors/key_sort_vectors.json new file mode 100644 index 00000000..de088a74 --- /dev/null +++ b/crates/musig2/src/test_vectors/key_sort_vectors.json @@ -0,0 +1,18 @@ +{ + "pubkeys": [ + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8" + ], + "sorted_pubkeys": [ + "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", + "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + ] +} diff --git a/crates/musig2/src/test_vectors/nonce_agg_vectors.json b/crates/musig2/src/test_vectors/nonce_agg_vectors.json new file mode 100644 index 00000000..1c04b881 --- /dev/null +++ b/crates/musig2/src/test_vectors/nonce_agg_vectors.json @@ -0,0 +1,51 @@ +{ + "pnonces": [ + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831", + "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" + ], + "valid_test_cases": [ + { + "pnonce_indices": [0, 1], + "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8" + }, + { + "pnonce_indices": [2, 3], + "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B000000000000000000000000000000000000000000000000000000000000000000", + "comment": "Sum of second points encoded in the nonces is point at infinity which is serialized as 33 zero bytes" + } + ], + "error_test_cases": [ + { + "pnonce_indices": [0, 4], + "error": { + "type": "invalid_contribution", + "signer": 1, + "contrib": "pubnonce" + }, + "comment": "Public nonce from signer 1 is invalid due wrong tag, 0x04, in the first half" + }, + { + "pnonce_indices": [5, 1], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubnonce" + }, + "comment": "Public nonce from signer 0 is invalid because the second half does not correspond to an X coordinate" + }, + { + "pnonce_indices": [6, 1], + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubnonce" + }, + "comment": "Public nonce from signer 0 is invalid because second half exceeds field size" + } + ] +} diff --git a/crates/musig2/src/test_vectors/nonce_gen_vectors.json b/crates/musig2/src/test_vectors/nonce_gen_vectors.json new file mode 100644 index 00000000..3a409c50 --- /dev/null +++ b/crates/musig2/src/test_vectors/nonce_gen_vectors.json @@ -0,0 +1,34 @@ +{ + "test_cases": [ + { + "rand": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", + "msg": "0101010101010101010101010101010101010101010101010101010101010101", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected_secnonce": "B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB6495B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "expected_pubnonce": "02F7BE7089E8376EB355272368766B17E88E7DB72047D05E56AA881EA52B3B35DF02C29C8046FDD0DED4C7E55869137200FBDBFE2EB654267B6D7013602CAED3115A" + }, + { + "rand": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", + "msg": "", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected_secnonce": "E862B068500320088138468D47E0E6F147E01B6024244AE45EAC40ACE5929B9F0789E051170B9E705D0B9EB49049A323BBBBB206D8E05C19F46C6228742AA7A9024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "expected_pubnonce": "023034FA5E2679F01EE66E12225882A7A48CC66719B1B9D3B6C4DBD743EFEDA2C503F3FD6F01EB3A8E9CB315D73F1F3D287CAFBB44AB321153C6287F407600205109" + }, + { + "rand": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", + "sk": "0202020202020202020202020202020202020202020202020202020202020202", + "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", + "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", + "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", + "expected_secnonce": "3221975ACBDEA6820EABF02A02B7F27D3A8EF68EE42787B88CBEFD9AA06AF3632EE85B1A61D8EF31126D4663A00DD96E9D1D4959E72D70FE5EBB6E7696EBA66F024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", + "expected_pubnonce": "02E5BBC21C69270F59BD634FCBFA281BE9D76601295345112C58954625BF23793A021307511C79F95D38ACACFF1B4DA98228B77E65AA216AD075E9673286EFB4EAF3" + } + ] +} diff --git a/crates/musig2/src/test_vectors/sig_agg_vectors.json b/crates/musig2/src/test_vectors/sig_agg_vectors.json new file mode 100644 index 00000000..04a7bc6b --- /dev/null +++ b/crates/musig2/src/test_vectors/sig_agg_vectors.json @@ -0,0 +1,151 @@ +{ + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02D2DC6F5DF7C56ACF38C7FA0AE7A759AE30E19B37359DFDE015872324C7EF6E05", + "03C7FB101D97FF930ACD0C6760852EF64E69083DE0B06AC6335724754BB4B0522C", + "02352433B21E7E05D3B452B81CAE566E06D2E003ECE16D1074AABA4289E0E3D581" + ], + "pnonces": [ + "036E5EE6E28824029FEA3E8A9DDD2C8483F5AF98F7177C3AF3CB6F47CAF8D94AE902DBA67E4A1F3680826172DA15AFB1A8CA85C7C5CC88900905C8DC8C328511B53E", + "03E4F798DA48A76EEC1C9CC5AB7A880FFBA201A5F064E627EC9CB0031D1D58FC5103E06180315C5A522B7EC7C08B69DCD721C313C940819296D0A7AB8E8795AC1F00", + "02C0068FD25523A31578B8077F24F78F5BD5F2422AFF47C1FADA0F36B3CEB6C7D202098A55D1736AA5FCC21CF0729CCE852575C06C081125144763C2C4C4A05C09B6", + "031F5C87DCFBFCF330DEE4311D85E8F1DEA01D87A6F1C14CDFC7E4F1D8C441CFA40277BF176E9F747C34F81B0D9F072B1B404A86F402C2D86CF9EA9E9C69876EA3B9", + "023F7042046E0397822C4144A17F8B63D78748696A46C3B9F0A901D296EC3406C302022B0B464292CF9751D699F10980AC764E6F671EFCA15069BBE62B0D1C62522A", + "02D97DDA5988461DF58C5897444F116A7C74E5711BF77A9446E27806563F3B6C47020CBAD9C363A7737F99FA06B6BE093CEAFF5397316C5AC46915C43767AE867C00" + ], + "tweaks": [ + "B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C", + "A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC", + "75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8" + ], + "psigs": [ + "B15D2CD3C3D22B04DAE438CE653F6B4ECF042F42CFDED7C41B64AAF9B4AF53FB", + "6193D6AC61B354E9105BBDC8937A3454A6D705B6D57322A5A472A02CE99FCB64", + "9A87D3B79EC67228CB97878B76049B15DBD05B8158D17B5B9114D3C226887505", + "66F82EA90923689B855D36C6B7E032FB9970301481B99E01CDB4D6AC7C347A15", + "4F5AEE41510848A6447DCD1BBC78457EF69024944C87F40250D3EF2C25D33EFE", + "DDEF427BBB847CC027BEFF4EDB01038148917832253EBC355FC33F4A8E2FCCE4", + "97B890A26C981DA8102D3BC294159D171D72810FDF7C6A691DEF02F0F7AF3FDC", + "53FA9E08BA5243CBCB0D797C5EE83BC6728E539EB76C2D0BF0F971EE4E909971", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869", + "valid_test_cases": [ + { + "aggnonce": "0341432722C5CD0268D829C702CF0D1CBCE57033EED201FD335191385227C3210C03D377F2D258B64AADC0E16F26462323D701D286046A2EA93365656AFD9875982B", + "nonce_indices": [ + 0, + 1 + ], + "key_indices": [ + 0, + 1 + ], + "tweak_indices": [], + "is_xonly": [], + "psig_indices": [ + 0, + 1 + ], + "expected": "041DA22223CE65C92C9A0D6C2CAC828AAF1EEE56304FEC371DDF91EBB2B9EF0912F1038025857FEDEB3FF696F8B99FA4BB2C5812F6095A2E0004EC99CE18DE1E" + }, + { + "aggnonce": "0224AFD36C902084058B51B5D36676BBA4DC97C775873768E58822F87FE437D792028CB15929099EEE2F5DAE404CD39357591BA32E9AF4E162B8D3E7CB5EFE31CB20", + "nonce_indices": [ + 0, + 2 + ], + "key_indices": [ + 0, + 2 + ], + "tweak_indices": [], + "is_xonly": [], + "psig_indices": [ + 2, + 3 + ], + "expected": "1069B67EC3D2F3C7C08291ACCB17A9C9B8F2819A52EB5DF8726E17E7D6B52E9F01800260A7E9DAC450F4BE522DE4CE12BA91AEAF2B4279219EF74BE1D286ADD9" + }, + { + "aggnonce": "0208C5C438C710F4F96A61E9FF3C37758814B8C3AE12BFEA0ED2C87FF6954FF186020B1816EA104B4FCA2D304D733E0E19CEAD51303FF6420BFD222335CAA402916D", + "nonce_indices": [ + 0, + 3 + ], + "key_indices": [ + 0, + 2 + ], + "tweak_indices": [ + 0 + ], + "is_xonly": [ + false + ], + "psig_indices": [ + 4, + 5 + ], + "expected": "5C558E1DCADE86DA0B2F02626A512E30A22CF5255CAEA7EE32C38E9A71A0E9148BA6C0E6EC7683B64220F0298696F1B878CD47B107B81F7188812D593971E0CC" + }, + { + "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD", + "nonce_indices": [ + 0, + 4 + ], + "key_indices": [ + 0, + 3 + ], + "tweak_indices": [ + 0, + 1, + 2 + ], + "is_xonly": [ + true, + false, + true + ], + "psig_indices": [ + 6, + 7 + ], + "expected": "839B08820B681DBA8DAF4CC7B104E8F2638F9388F8D7A555DC17B6E6971D7426CE07BF6AB01F1DB50E4E33719295F4094572B79868E440FB3DEFD3FAC1DB589E" + } + ], + "error_test_cases": [ + { + "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD", + "nonce_indices": [ + 0, + 4 + ], + "key_indices": [ + 0, + 3 + ], + "tweak_indices": [ + 0, + 1, + 2 + ], + "is_xonly": [ + true, + false, + true + ], + "psig_indices": [ + 7, + 8 + ], + "error": { + "type": "invalid_contribution", + "signer": 1 + }, + "comment": "Partial signature is invalid because it exceeds group size" + } + ] +} diff --git a/crates/musig2/src/test_vectors/sign_verify_vectors.json b/crates/musig2/src/test_vectors/sign_verify_vectors.json new file mode 100644 index 00000000..b467640c --- /dev/null +++ b/crates/musig2/src/test_vectors/sign_verify_vectors.json @@ -0,0 +1,212 @@ +{ + "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA661", + "020000000000000000000000000000000000000000000000000000000000000007" + ], + "secnonces": [ + "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" + ], + "pnonces": [ + "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046", + "0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480" + ], + "aggnonces": [ + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", + "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" + ], + "msgs": [ + "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "", + "2626262626262626262626262626262626262626262626262626262626262626262626262626" + ], + "valid_test_cases": [ + { + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 0, + "expected": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB" + }, + { + "key_indices": [1, 0, 2], + "nonce_indices": [1, 0, 2], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 1, + "expected": "9FF2F7AAA856150CC8819254218D3ADEEB0535269051897724F9DB3789513A52" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 2, + "expected": "FA23C359F6FAC4E7796BB93BC9F0532A95468C539BA20FF86D7C76ED92227900" + }, + { + "key_indices": [0, 1], + "nonce_indices": [0, 3], + "aggnonce_index": 1, + "msg_index": 0, + "signer_index": 0, + "expected": "AE386064B26105404798F75DE2EB9AF5EDA5387B064B83D049CB7C5E08879531", + "comment": "Both halves of aggregate nonce correspond to point at infinity" + }, + { + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 1, + "signer_index": 0, + "expected": "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D", + "comment": "Empty message" + }, + { + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 2, + "signer_index": 0, + "expected": "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C", + "comment": "38-byte message" + } + ], + "sign_error_test_cases": [ + { + "key_indices": [1, 2], + "aggnonce_index": 0, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "value", + "message": "The signer's pubkey must be included in the list of pubkeys." + }, + "comment": "The signers pubkey is not in the list of pubkeys. This test case is optional: it can be skipped by implementations that do not check that the signer's pubkey is included in the list of pubkeys." + }, + { + "key_indices": [1, 0, 3], + "aggnonce_index": 0, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": 2, + "contrib": "pubkey" + }, + "comment": "Signer 2 provided an invalid public key" + }, + { + "key_indices": [1, 2, 0], + "aggnonce_index": 2, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggnonce" + }, + "comment": "Aggregate nonce is invalid due wrong tag, 0x04, in the first half" + }, + { + "key_indices": [1, 2, 0], + "aggnonce_index": 3, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggnonce" + }, + "comment": "Aggregate nonce is invalid because the second half does not correspond to an X coordinate" + }, + { + "key_indices": [1, 2, 0], + "aggnonce_index": 4, + "msg_index": 0, + "secnonce_index": 0, + "error": { + "type": "invalid_contribution", + "signer": null, + "contrib": "aggnonce" + }, + "comment": "Aggregate nonce is invalid because second half exceeds field size" + }, + { + "key_indices": [0, 1, 2], + "aggnonce_index": 0, + "msg_index": 0, + "signer_index": 0, + "secnonce_index": 1, + "error": { + "type": "value", + "message": "first secnonce value is out of range." + }, + "comment": "Secnonce is invalid which may indicate nonce reuse" + } + ], + "verify_fail_test_cases": [ + { + "sig": "97AC833ADCB1AFA42EBF9E0725616F3C9A0D5B614F6FE283CEAAA37A8FFAF406", + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 0, + "comment": "Wrong signature (which is equal to the negation of valid signature)" + }, + { + "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 1, + "comment": "Wrong signer" + }, + { + "sig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", + "key_indices": [0, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 0, + "comment": "Signature exceeds group size" + } + ], + "verify_error_test_cases": [ + { + "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", + "key_indices": [0, 1, 2], + "nonce_indices": [4, 1, 2], + "msg_index": 0, + "signer_index": 0, + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubnonce" + }, + "comment": "Invalid pubnonce" + }, + { + "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", + "key_indices": [3, 1, 2], + "nonce_indices": [0, 1, 2], + "msg_index": 0, + "signer_index": 0, + "error": { + "type": "invalid_contribution", + "signer": 0, + "contrib": "pubkey" + }, + "comment": "Invalid pubkey" + } + ] +} diff --git a/crates/musig2/src/test_vectors/tweak_vectors.json b/crates/musig2/src/test_vectors/tweak_vectors.json new file mode 100644 index 00000000..d0a7cfe8 --- /dev/null +++ b/crates/musig2/src/test_vectors/tweak_vectors.json @@ -0,0 +1,84 @@ +{ + "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", + "pubkeys": [ + "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", + "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" + ], + "secnonce": "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", + "pnonces": [ + "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", + "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", + "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046" + ], + "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", + "tweaks": [ + "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", + "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", + "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", + "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ], + "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", + "valid_test_cases": [ + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0], + "is_xonly": [true], + "signer_index": 2, + "expected": "E28A5C66E61E178C2BA19DB77B6CF9F7E2F0F56C17918CD13135E60CC848FE91", + "comment": "A single x-only tweak" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0], + "is_xonly": [false], + "signer_index": 2, + "expected": "38B0767798252F21BF5702C48028B095428320F73A4B14DB1E25DE58543D2D2D", + "comment": "A single plain tweak" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0, 1], + "is_xonly": [false, true], + "signer_index": 2, + "expected": "408A0A21C4A0F5DACAF9646AD6EB6FECD7F7A11F03ED1F48DFFF2185BC2C2408", + "comment": "A plain tweak followed by an x-only tweak" + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [false, false, true, true], + "signer_index": 2, + "expected": "45ABD206E61E3DF2EC9E264A6FEC8292141A633C28586388235541F9ADE75435", + "comment": "Four tweaks: plain, plain, x-only, x-only." + }, + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [0, 1, 2, 3], + "is_xonly": [true, false, true, false], + "signer_index": 2, + "expected": "B255FDCAC27B40C7CE7848E2D3B7BF5EA0ED756DA81565AC804CCCA3E1D5D239", + "comment": "Four tweaks: x-only, plain, x-only, plain. If an implementation prohibits applying plain tweaks after x-only tweaks, it can skip this test vector or return an error." + } + ], + "error_test_cases": [ + { + "key_indices": [1, 2, 0], + "nonce_indices": [1, 2, 0], + "tweak_indices": [4], + "is_xonly": [false], + "signer_index": 2, + "error": { + "type": "value", + "message": "The tweak must be less than n." + }, + "comment": "Tweak is invalid because it exceeds group size" + } + ] +} diff --git a/crates/musig2/src/testhex.rs b/crates/musig2/src/testhex.rs new file mode 100644 index 00000000..ec315e6d --- /dev/null +++ b/crates/musig2/src/testhex.rs @@ -0,0 +1,93 @@ +//! This code is used to assist in deserializing test vectors. + +use serde::Deserialize; + +#[derive(Debug)] +pub struct FromBytesError { + pub unexpected: String, + pub expected: String, +} + +pub trait TryFromBytes: Sized { + fn try_from_bytes(bytes: &[u8]) -> Result; +} + +impl TryFromBytes for Vec { + fn try_from_bytes(bytes: &[u8]) -> Result { + Ok(Vec::from(bytes)) + } +} + +impl TryFromBytes for [u8; SIZE] { + fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != SIZE { + return Err(FromBytesError { + expected: format!("byte vector of length {}", SIZE), + unexpected: format!("byte vector of length {}", bytes.len()), + }); + } + + let mut array = [0; SIZE]; + array[..].clone_from_slice(bytes); + Ok(array) + } +} + +struct HexVisitor; + +impl<'de> serde::de::Visitor<'de> for HexVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a hex string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + base16ct::mixed::decode_vec(v) + .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &"a hex string")) + } +} + +struct HexString(pub T); + +impl<'de, T: TryFromBytes> Deserialize<'de> for HexString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let vec = deserializer.deserialize_str(HexVisitor)?; + + let parsed = T::try_from_bytes(&vec).map_err(|e| { + ::invalid_value( + serde::de::Unexpected::Other(&e.unexpected), + &e.expected.as_str(), + ) + })?; + + Ok(HexString(parsed)) + } +} + +pub fn deserialize<'de, D, T>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, + T: TryFromBytes, +{ + let HexString(value) = HexString::::deserialize(deserializer)?; + Ok(value) +} + +pub fn deserialize_vec<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, + T: TryFromBytes, +{ + let items = >>::deserialize(deserializer)? + .into_iter() + .map(|HexString(value)| value) + .collect(); + Ok(items) +} diff --git a/crates/musig2/tests/fuzz_against_reference_impl.rs b/crates/musig2/tests/fuzz_against_reference_impl.rs new file mode 100644 index 00000000..68bf0aa7 --- /dev/null +++ b/crates/musig2/tests/fuzz_against_reference_impl.rs @@ -0,0 +1,317 @@ +use rand::Rng; +use secp::{Point, Scalar}; + +use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce, SCHNORR_SIGNATURE_SIZE}; + +fn run_reference_code(code: &str) -> Vec { + let script = [ + "import reference", + "from binascii import hexlify, unhexlify", + "from sys import stdout\n\n", + ] + .join("\n") + + code; + + let error_message = format!("failed to run reference code:\n{}", code); + + let output = std::process::Command::new("python3") + .arg("-c") + .arg(script) + .output() + .expect(&error_message); + + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!( + output.status.success(), + "{}\nstderr: {}", + error_message, + stderr + ); + + output.stdout +} + +#[test] +fn test_python_interop() { + let output = String::from_utf8(run_reference_code("print('hello world')")).unwrap(); + assert_eq!(output, "hello world\n"); +} + +fn random_sample_indexes( + rng: &mut R, + iterations: usize, + max_count: usize, + index_ceil: usize, +) -> Vec> { + (0..iterations) + .map(|_| { + let count = rng.gen_range(1..max_count); + (0..count) + .map(|_| rng.gen_range(0..index_ceil)) + .collect::>() + }) + .collect() +} + +/// Runs our key aggregation code against the reference implementation using randomly +/// chosen pubkey inputs. +#[test] +fn test_key_aggregation() { + let mut rng = rand::thread_rng(); + + // Initialize a random array of pubkeys. + let mut all_pubkeys = [Point::generator(); 6]; + for pubkey in &mut all_pubkeys { + *pubkey *= Scalar::random(&mut rng); + } + + const ITERATIONS: usize = 5; + const MAX_PUBKEYS: usize = 8; + + // randomly sample indexes into that array, with at least length 1 + let generated_indexes: Vec> = + random_sample_indexes(&mut rng, ITERATIONS, MAX_PUBKEYS, all_pubkeys.len()); + + let all_pubkeys_json = serde_json::to_string(&all_pubkeys).unwrap(); + let generated_indexes_json = serde_json::to_string(&generated_indexes).unwrap(); + + let reference_code_output = run_reference_code(&format!( + r#" +all_pubkeys = [unhexlify(key) for key in {all_pubkeys_json}] +generated_indexes = {generated_indexes_json} + +for indexes in generated_indexes: + pubkeys = [all_pubkeys[i] for i in indexes] + Q = reference.key_agg(pubkeys)[0] + stdout.buffer.write(reference.cbytes(Q)) +"# + )); + assert_eq!( + reference_code_output.len(), + ITERATIONS * 33, + "expected to receive exactly {} * 33 bytes back from reference impl", + ITERATIONS + ); + + for i in 0..ITERATIONS { + let pubkeys: Vec = generated_indexes[i] + .iter() + .map(|&j| all_pubkeys[j]) + .collect(); + + let expected_pubkey_bytes = &reference_code_output[(i * 33)..(i * 33 + 33)]; + let expected_pubkey = Point::from_slice(expected_pubkey_bytes).unwrap_or_else(|_| { + panic!( + "error decoding aggregated public key from reference implementation: {}", + base16ct::lower::encode_string(expected_pubkey_bytes) + ) + }); + + let pubkeys_json = serde_json::to_string(&pubkeys).unwrap(); + + let our_pubkey: Point = KeyAggContext::new(pubkeys) + .unwrap_or_else(|_| panic!("failed to aggregate pubkeys: {}", pubkeys_json)) + .aggregated_pubkey(); + + assert_eq!( + our_pubkey, expected_pubkey, + "aggregated pubkey does not match reference impl for inputs: {}", + pubkeys_json + ); + } +} + +/// Runs our partial signing and signature aggregation code against +/// the reference implementation. +#[test] +fn test_signing() { + let mut rng = rand::thread_rng(); + + let mut all_seckeys = [Scalar::one(); 4]; + for seckey in &mut all_seckeys { + *seckey = Scalar::random(&mut rng); + } + + let all_pubkeys = all_seckeys + .into_iter() + .map(|sk| sk.base_point_mul()) + .collect::>(); + + let all_nonce_seeds = (0..all_seckeys.len()) + .map(|_| Scalar::random(&mut rng)) + .collect::>(); + + let message = "Welcome to MuSig"; + + const ITERATIONS: usize = 5; + const MAX_SIGNERS: usize = 5; + + let all_key_indexes = + random_sample_indexes(&mut rng, ITERATIONS, MAX_SIGNERS, all_seckeys.len()); + + let all_aggregated_pubkeys = (0..ITERATIONS) + .map(|i| { + let pubkeys = all_key_indexes[i].iter().map(|&j| all_pubkeys[j]); + KeyAggContext::new(pubkeys).unwrap().aggregated_pubkey() + }) + .collect::>(); + + let all_seckeys_json = serde_json::to_string(&all_seckeys).unwrap(); + let all_pubkeys_json = serde_json::to_string(&all_pubkeys).unwrap(); + let all_nonce_seeds_json = serde_json::to_string(&all_nonce_seeds).unwrap(); + let all_key_indexes_json = serde_json::to_string(&all_key_indexes).unwrap(); + let all_aggregated_pubkeys_json = serde_json::to_string(&all_aggregated_pubkeys).unwrap(); + + let reference_code_output = run_reference_code(&format!( + r#" +all_seckeys = [unhexlify(key) for key in {all_seckeys_json}] +all_pubkeys = [unhexlify(key) for key in {all_pubkeys_json}] +all_nonce_seeds = [unhexlify(seed) for seed in {all_nonce_seeds_json}] +all_key_indexes = {all_key_indexes_json} +all_aggregated_pubkeys = [unhexlify(b) for b in {all_aggregated_pubkeys_json}] + +message = b"{message}" + +for i in range({ITERATIONS}): + aggregated_pubkey = all_aggregated_pubkeys[i] + key_indexes = all_key_indexes[i] + seckeys = [all_seckeys[j] for j in key_indexes] + pubkeys = [all_pubkeys[j] for j in key_indexes] + nonce_seeds = [all_nonce_seeds[j] for j in key_indexes] + + nonces = [ + reference.nonce_gen_internal( + nonce_seeds[k], + seckeys[k], + pubkeys[k], + aggregated_pubkey[1:], + message, + k.to_bytes(4, 'big') + ) + for k in range(len(key_indexes)) + ] + + aggnonce = reference.nonce_agg([pubnonce for (_, pubnonce) in nonces]) + session_ctx = (aggnonce, pubkeys, [], [], message) + + partial_signatures = [] + for ((secnonce, _), seckey) in zip(nonces, seckeys): + partial_signature = reference.sign(secnonce, seckey, session_ctx) + stdout.buffer.write(partial_signature) + partial_signatures.append(partial_signature) + + final_signature = reference.partial_sig_agg(partial_signatures, session_ctx) + stdout.buffer.write(final_signature) +"# + )); + + let n_partial_signatures = all_key_indexes + .iter() + .map(|indexes| indexes.len()) + .sum::(); + + assert_eq!( + reference_code_output.len(), + n_partial_signatures * 32 + SCHNORR_SIGNATURE_SIZE * ITERATIONS, + "expected {} partial signatures and {} aggregated signatures from reference \ + implementation, got {} bytes", + n_partial_signatures, + ITERATIONS, + reference_code_output.len() + ); + + let mut cursor = 0usize; + + for i in 0..ITERATIONS { + let key_indexes = all_key_indexes[i].clone(); + let seckeys = key_indexes + .iter() + .map(|&j| all_seckeys[j]) + .collect::>(); + let pubkeys = key_indexes + .iter() + .map(|&j| all_pubkeys[j]) + .collect::>(); + let nonce_seeds = key_indexes + .iter() + .map(|&j| all_nonce_seeds[j]) + .collect::>(); + + let debug_json = serde_json::to_string(&serde_json::json!({ + "seckeys": &seckeys, + "nonce_seeds": &nonce_seeds, + })) + .unwrap(); + + let aggregated_pubkey = all_aggregated_pubkeys[i]; + let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); + assert_eq!(key_agg_ctx.aggregated_pubkey::(), aggregated_pubkey); + + let secnonces: Vec = nonce_seeds + .into_iter() + .enumerate() + .map(|(k, seed)| { + SecNonce::generate( + seed.serialize(), + seckeys[k], + aggregated_pubkey, + message, + (k as u32).to_be_bytes(), + ) + }) + .collect(); + + let aggnonce = secnonces + .iter() + .map(|secnonce| secnonce.public_nonce()) + .sum::(); + + let mut partial_signatures = Vec::with_capacity(seckeys.len()); + for k in 0..seckeys.len() { + let our_partial_signature: PartialSignature = musig2::sign_partial( + &key_agg_ctx, + seckeys[k], + secnonces[k].clone(), + &aggnonce, + message, + ) + .unwrap_or_else(|_| { + panic!( + "failed to sign with randomly chosen keys and nonces: {}", + debug_json + ) + }); + + let expected_partial_signature_bytes = &reference_code_output[cursor..cursor + 32]; + cursor += 32; + + assert_eq!( + &our_partial_signature.serialize(), + expected_partial_signature_bytes, + "incorrect partial signature for signer index {} using keys and nonces: {}", + k, + debug_json, + ); + + partial_signatures.push(our_partial_signature); + } + let expected_signature_bytes = + &reference_code_output[cursor..cursor + SCHNORR_SIGNATURE_SIZE]; + cursor += SCHNORR_SIGNATURE_SIZE; + + let our_signature: [u8; SCHNORR_SIGNATURE_SIZE] = musig2::aggregate_partial_signatures( + &key_agg_ctx, + &aggnonce, + partial_signatures, + message, + ) + .expect("error aggregating partial signatures"); + + assert_eq!( + &our_signature, expected_signature_bytes, + "incorrect aggregated signature using keys and nonces: {}", + debug_json + ); + } +} diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 2b1286b8..5a0a1898 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -109,6 +109,11 @@ pub trait WotsSigner: Send { fn get_key(&self, index: u64) -> impl Future> + Send; } +pub trait StakeChainPreimages: Send { + fn get_preimg(&self, deposit_index: u64) + -> impl Future> + Send; +} + pub trait Origin { type Container; } diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index ed8ea30b..fdd2abc5 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -38,9 +38,9 @@ use tokio::{ use tracing::{error, span, warn, Instrument, Level}; pub struct Config { - addr: SocketAddr, - connection_limit: Option, - tls_config: rustls::ServerConfig, + pub addr: SocketAddr, + pub connection_limit: Option, + pub tls_config: rustls::ServerConfig, } pub struct ServerHandle { diff --git a/crates/secret-service-server/src/ms2sm.rs b/crates/secret-service-server/src/ms2sm.rs index b47f136d..2a8c8aa0 100644 --- a/crates/secret-service-server/src/ms2sm.rs +++ b/crates/secret-service-server/src/ms2sm.rs @@ -44,8 +44,8 @@ pub struct OutOfRange; #[derive(Debug)] pub struct NotInCorrectRound { - wanted: SlotState, - got: SlotState, + pub wanted: SlotState, + pub got: SlotState, } #[derive(Debug)] @@ -177,7 +177,7 @@ where } #[derive(Debug)] -enum SlotState { +pub enum SlotState { Empty, FirstRound, SecondRound, diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index c7ed7d1e..accfa278 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -7,8 +7,15 @@ edition = "2021" bitcoin.workspace = true blake3.workspace = true musig2.workspace = true +rcgen = "0.13.2" +rustls-pemfile = "2.2.0" secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } secret-service-server = { version = "0.1.0", path = "../secret-service-server" } +serde.workspace = true +sled = "0.34.7" strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } tokio.workspace = true +toml = "0.8.19" +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 7e562feb..390550ed 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,8 +1,217 @@ // use secret_service_server::rustls::ServerConfig; +use std::{ + env::args, + fs, + future::Future, + net::SocketAddr, + path::{Path, PathBuf}, + str::FromStr, +}; + +use secret_service_proto::v1::traits::{ + Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, Origin, SecretService, Server, +}; +use secret_service_server::{ + run_server, + rustls::{ + pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, + ServerConfig, + }, + Config, +}; +use tracing::info; + #[tokio::main] async fn main() { // let config = ServerConfig::builder() // .with_client_cert_verifier(client_cert_verifier) - println!("Hello, world!"); + // let config = Config { + // addr: SocketAddr::V4(()) + // } + // run_server(c, service) + let config_path = + PathBuf::from_str(&args().nth(1).unwrap_or_else(|| "config.toml".to_string())) + .expect("valid config path"); + + let text = std::fs::read_to_string(&config_path).expect("read config file"); + let conf: TomlConfig = toml::from_str(&text).expect("valid toml"); + + let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&conf.key, &conf.cert) { + let key = fs::read(key_path).expect("readable key"); + let key = if key_path.extension().is_some_and(|x| x == "der") { + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) + } else { + rustls_pemfile::private_key(&mut &*key) + .expect("valid PEM-encoded private key") + .expect("non-empty private key") + }; + let cert_chain = fs::read(cert_path).expect("readable certificate"); + let cert_chain = if cert_path.extension().is_some_and(|x| x == "der") { + vec![CertificateDer::from(cert_chain)] + } else { + rustls_pemfile::certs(&mut &*cert_chain) + .collect::>() + .expect("valid PEM-encoded certificate") + }; + + (cert_chain, key) + } else { + info!("using self-signed certificate"); + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + let cert = cert.cert.into(); + (vec![cert], key.into()) + }; + + let tls = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(certs, key) + .expect("valid rustls config"); + + let config = Config { + addr: conf.addr, + tls_config: tls, + connection_limit: conf.conn_limit, + }; + + run_server(config, Service.into()).unwrap().await; +} + +#[derive(serde::Deserialize)] +struct TomlConfig { + addr: SocketAddr, + conn_limit: Option, + cert: Option, + key: Option, +} + +struct Service; + +struct FirstRound; + +impl Musig2SignerFirstRound for FirstRound { + fn our_nonce( + &self, + ) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send + { + async move { todo!() } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn receive_pub_nonce( + &self, + pubkey: musig2::secp256k1::PublicKey, + pubnonce: musig2::PubNonce, + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { todo!() } + } + + fn finalize( + self, + hash: [u8; 32], + ) -> impl Future< + Output = ::Container< + Result, + >, + > + Send { + async move { todo!() } + } +} + +struct SecondRound; + +impl Musig2SignerSecondRound for SecondRound { + fn agg_nonce( + &self, + ) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send + { + async move { todo!() } + } + + fn our_signature( + &self, + ) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { todo!() } + } + + fn receive_signature( + &self, + pubkey: musig2::secp256k1::PublicKey, + signature: musig2::PartialSignature, + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { todo!() } + } + + fn finalize( + self, + ) -> impl Future< + Output = ::Container< + Result, + >, + > + Send { + async move { todo!() } + } +} + +struct Operator; + +impl OperatorSigner for Operator { + fn sign_psbt( + &self, + psbt: bitcoin::Psbt, + ) -> impl Future> + Send { + async move { todo!() } + } +} + +struct P2PSigner; + +impl SecretService for Service { + type OperatorSigner = Operator; + + type P2PSigner; + + type Musig2Signer; + + type WotsSigner; + + fn operator_signer(&self) -> Self::OperatorSigner { + todo!() + } + + fn p2p_signer(&self) -> Self::P2PSigner { + todo!() + } + + fn musig2_signer(&self) -> Self::Musig2Signer { + todo!() + } + + fn wots_signer(&self) -> Self::WotsSigner { + todo!() + } } From 5299ddb8c936a58bc4964b24aa7fb9a1a7adf0c6 Mon Sep 17 00:00:00 2001 From: Azz Date: Wed, 5 Feb 2025 11:46:56 +0000 Subject: [PATCH 08/30] stakechain, binary, others --- crates/musig2/src/rkyv_wrappers.rs | 6 +- crates/secret-service-client/Cargo.toml | 2 +- crates/secret-service-client/src/lib.rs | 43 +++- crates/secret-service-proto/Cargo.toml | 2 +- crates/secret-service-proto/src/v1/traits.rs | 9 +- crates/secret-service-proto/src/v1/wire.rs | 12 +- crates/secret-service-server/Cargo.toml | 2 +- crates/secret-service-server/src/lib.rs | 83 ++++++- crates/secret-service-server/src/ms2sm.rs | 46 +++- crates/secret-service/Cargo.toml | 6 +- crates/secret-service/src/disk/mod.rs | 5 + crates/secret-service/src/disk/musig2.rs | 0 crates/secret-service/src/disk/operator.rs | 0 crates/secret-service/src/disk/p2p.rs | 0 crates/secret-service/src/disk/stakechain.rs | 0 crates/secret-service/src/disk/wots.rs | 0 crates/secret-service/src/main.rs | 244 +++++++++++++++---- 17 files changed, 376 insertions(+), 84 deletions(-) create mode 100644 crates/secret-service/src/disk/mod.rs create mode 100644 crates/secret-service/src/disk/musig2.rs create mode 100644 crates/secret-service/src/disk/operator.rs create mode 100644 crates/secret-service/src/disk/p2p.rs create mode 100644 crates/secret-service/src/disk/stakechain.rs create mode 100644 crates/secret-service/src/disk/wots.rs diff --git a/crates/musig2/src/rkyv_wrappers.rs b/crates/musig2/src/rkyv_wrappers.rs index d87056b7..d864ffb0 100644 --- a/crates/musig2/src/rkyv_wrappers.rs +++ b/crates/musig2/src/rkyv_wrappers.rs @@ -2,7 +2,7 @@ use rkyv::{Archive, Deserialize, Serialize}; use secp256k1::ffi::CPtr; #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp::Point)] +#[rkyv(remote = secp::Point, derive(Hash, PartialEq, Eq))] pub struct Point { #[rkyv(getter = point_inner_getter, with = PublicKey)] inner: secp256k1::PublicKey, @@ -29,7 +29,7 @@ impl From for Point { #[derive( Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Archive, Serialize, Deserialize, )] -#[rkyv(remote = secp256k1::PublicKey)] +#[rkyv(remote = secp256k1::PublicKey, derive(Hash, PartialEq, Eq))] pub struct PublicKey( #[rkyv(getter = public_key_getter, with = FFIPublicKey)] secp256k1::ffi::PublicKey, ); @@ -51,7 +51,7 @@ impl From for PublicKey { } #[derive(Copy, Clone, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp256k1::ffi::PublicKey)] +#[rkyv(remote = secp256k1::ffi::PublicKey, derive(Hash, PartialEq, Eq))] pub struct FFIPublicKey(#[rkyv(getter = ffi_public_key_getter)] [u8; 64]); fn ffi_public_key_getter(p: &secp256k1::ffi::PublicKey) -> [u8; 64] { diff --git a/crates/secret-service-client/Cargo.toml b/crates/secret-service-client/Cargo.toml index e963707a..e01ceeb2 100644 --- a/crates/secret-service-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] bitcoin.workspace = true kanal.workspace = true -musig2.workspace = true +musig2 = { path = "../musig2" } quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index ca332409..bb674258 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -21,7 +21,8 @@ use secret_service_proto::{ v1::{ traits::{ Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, - Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, WotsSigner, + Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, + StakeChainPreimages, WotsSigner, }, wire::{ClientMessage, ServerMessage}, }, @@ -96,6 +97,8 @@ impl SecretService for SecretServic type WotsSigner = WotsClient; + type StakeChain = StakeChainClient; + fn operator_signer(&self) -> Self::OperatorSigner { OperatorClient { conn: self.conn.clone(), @@ -123,8 +126,16 @@ impl SecretService for SecretServic config: self.config.clone(), } } + + fn stake_chain(&self) -> Self::StakeChain { + StakeChainClient { + conn: self.conn.clone(), + config: self.config.clone(), + } + } } +#[derive(Clone)] struct Musig2FirstRound { session_id: Musig2SessionId, connection: Connection, @@ -398,9 +409,14 @@ struct Musig2Client { } impl Musig2Signer for Musig2Client { - fn new_session(&self) -> impl Future> + Send { + fn new_session( + &self, + public_keys: Vec, + ) -> impl Future> + Send { async move { - let msg = ClientMessage::Musig2NewSession; + let msg = ClientMessage::Musig2NewSession { + public_keys: public_keys.into_iter().map(|pk| pk.serialize()).collect(), + }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2NewSession { session_id } = res else { return Err(ClientError::ProtocolError(res)); @@ -436,6 +452,27 @@ impl WotsSigner for WotsClient { } } +struct StakeChainClient { + conn: Connection, + config: Arc, +} + +impl StakeChainPreimages for StakeChainClient { + fn get_preimg( + &self, + deposit_idx: u64, + ) -> impl Future::Container<[u8; 32]>> + Send { + async move { + let msg = ClientMessage::StakeChainGetPreimage { deposit_idx }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::StakeChainGetPreimage { preimg } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(preimg) + } + } +} + async fn make_v1_req( conn: &Connection, msg: ClientMessage, diff --git a/crates/secret-service-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml index 03952a66..54ec29ba 100644 --- a/crates/secret-service-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" [dependencies] bitcoin.workspace = true -musig2.workspace = true +musig2 = { path = "../musig2" } quinn.workspace = true rkyv.workspace = true diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 5a0a1898..8a6a78e7 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -13,7 +13,7 @@ use super::wire::ServerMessage; pub trait SecretServiceFactory: Send + Clone where - FirstRound: Musig2SignerFirstRound, + FirstRound: Musig2SignerFirstRound + Clone, SecondRound: Musig2SignerSecondRound, { type Context: Send + Clone; @@ -33,11 +33,13 @@ where type P2PSigner: P2PSigner; type Musig2Signer: Musig2Signer; type WotsSigner: WotsSigner; + type StakeChain: StakeChainPreimages; fn operator_signer(&self) -> Self::OperatorSigner; fn p2p_signer(&self) -> Self::P2PSigner; fn musig2_signer(&self) -> Self::Musig2Signer; fn wots_signer(&self) -> Self::WotsSigner; + fn stake_chain(&self) -> Self::StakeChain; } pub trait OperatorSigner: Send { @@ -63,7 +65,10 @@ pub trait P2PSigner: Send { pub type Musig2SessionId = usize; pub trait Musig2Signer: Send + Sync { - fn new_session(&self) -> impl Future> + Send; + fn new_session( + &self, + public_keys: Vec, + ) -> impl Future> + Send; } pub trait Musig2SignerFirstRound: Send + Sync { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index d3d3f68f..2402082e 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -61,6 +61,10 @@ pub enum ServerMessage { WotsGetKey { key: [u8; 64], }, + + StakeChainGetPreimage { + preimg: [u8; 32], + }, } #[derive(Debug, Clone, Archive, Serialize, Deserialize)] @@ -100,7 +104,9 @@ pub enum ClientMessage { }, P2PPubkey, - Musig2NewSession, + Musig2NewSession { + public_keys: Vec<[u8; 33]>, + }, Musig2FirstRoundOurNonce { session_id: usize, @@ -145,4 +151,8 @@ pub enum ClientMessage { WotsGetKey { index: u64, }, + + StakeChainGetPreimage { + deposit_idx: u64, + }, } diff --git a/crates/secret-service-server/Cargo.toml b/crates/secret-service-server/Cargo.toml index ecd3599c..dece1dc5 100644 --- a/crates/secret-service-server/Cargo.toml +++ b/crates/secret-service-server/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" bitcoin.workspace = true futures.workspace = true kanal.workspace = true -musig2.workspace = true +musig2 = { path = "../musig2" } parking_lot.workspace = true quinn.workspace = true rkyv.workspace = true diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index fdd2abc5..7803c9d6 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -2,6 +2,7 @@ pub mod bool_arr; pub mod ms2sm; use std::{ + fmt::Debug, future::Future, io, marker::Sync, @@ -23,8 +24,8 @@ use rkyv::rancor::Error; use secret_service_proto::{ v1::{ traits::{ - Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, - P2PSigner, SecretService, Server, WotsSigner, + Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, + OperatorSigner, P2PSigner, SecretService, Server, StakeChainPreimages, WotsSigner, }, wire::{ArchivedClientMessage, ServerMessage}, }, @@ -60,9 +61,11 @@ pub fn run_server( service: Arc, ) -> Result> where - FirstRound: Musig2SignerFirstRound + 'static, - SecondRound: Musig2SignerSecondRound + 'static, + FirstRound: Musig2SignerFirstRound + 'static + RoundPersister, + SecondRound: Musig2SignerSecondRound + 'static + RoundPersister, Service: SecretService + Sync + 'static, + >::Musig2Signer: + Musig2RoundRecovery, { let quic_server_config = ServerConfig::with_crypto(Arc::new( QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, @@ -97,9 +100,11 @@ async fn conn_handler( service: Arc, musig2_sm: Arc>>, ) where - FirstRound: Musig2SignerFirstRound + 'static, - SecondRound: Musig2SignerSecondRound + 'static, + FirstRound: Musig2SignerFirstRound + 'static + RoundPersister, + SecondRound: Musig2SignerSecondRound + 'static + RoundPersister, Service: SecretService + Sync + 'static, + >::Musig2Signer: + Musig2RoundRecovery, { let conn = match incoming.await { Ok(conn) => conn, @@ -172,9 +177,11 @@ async fn request_handler( musig2_sm: Arc>>, ) -> Result where - FirstRound: Musig2SignerFirstRound, - SecondRound: Musig2SignerSecondRound, + FirstRound: Musig2SignerFirstRound + RoundPersister, + SecondRound: Musig2SignerSecondRound + RoundPersister, Service: SecretService, + >::Musig2Signer: + Musig2RoundRecovery, { let len_to_read = { let mut buf = [0; size_of::()]; @@ -207,11 +214,35 @@ where ServerMessage::P2PPubkey { pubkey } } - ArchivedClientMessage::Musig2NewSession => { - let first_round = service.musig2_signer().new_session().await; - match musig2_sm.lock().await.new_session(first_round) { - Some(session_id) => ServerMessage::Musig2NewSession { session_id }, - None => ServerMessage::OpaqueServerError, + ArchivedClientMessage::Musig2NewSession { public_keys } => 'block: { + let signer = service.musig2_signer(); + let public_keys: Result, _> = public_keys + .iter() + .map(AsRef::<[u8]>::as_ref) + .map(PublicKey::from_slice) + .collect(); + + let Ok(mut public_keys) = public_keys else { + break 'block ServerMessage::InvalidClientMessage; + }; + + // enforce sorting at the protocol level + public_keys.sort(); + + let first_round = signer.new_session(public_keys).await; + let mut sm = musig2_sm.lock().await; + + let Ok(write_perm) = sm.new_session(first_round) else { + break 'block ServerMessage::OpaqueServerError; + }; + + if let Err(e) = write_perm.value().persist(write_perm.session_id()).await { + error!("failed to persist first round: {e:?}"); + break 'block ServerMessage::OpaqueServerError; + } + + ServerMessage::Musig2NewSession { + session_id: write_perm.session_id(), } } ArchivedClientMessage::Musig2FirstRoundOurNonce { session_id } => { @@ -395,6 +426,32 @@ where let key = service.wots_signer().get_key(index.into()).await; ServerMessage::WotsGetKey { key } } + + ArchivedClientMessage::StakeChainGetPreimage { deposit_idx } => { + let preimg = service.stake_chain().get_preimg(deposit_idx.into()).await; + ServerMessage::StakeChainGetPreimage { preimg } + } }, }) } + +pub trait RoundPersister { + type Error: Debug; + + fn persist( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send; +} + +pub trait Musig2RoundRecovery { + type Error: Debug; + + fn load_first_rounds( + &self, + ) -> impl Future, Self::Error>> + Send; + + fn load_second_rounds( + &self, + ) -> impl Future, Self::Error>> + Send; +} diff --git a/crates/secret-service-server/src/ms2sm.rs b/crates/secret-service-server/src/ms2sm.rs index 2a8c8aa0..31babc21 100644 --- a/crates/secret-service-server/src/ms2sm.rs +++ b/crates/secret-service-server/src/ms2sm.rs @@ -1,4 +1,4 @@ -use std::{mem::MaybeUninit, sync::Arc}; +use std::{mem::MaybeUninit, ptr, sync::Arc}; use musig2::{errors::RoundFinalizeError, LiftedSignature}; use secret_service_proto::v1::traits::{Musig2SignerFirstRound, Musig2SignerSecondRound, Server}; @@ -51,21 +51,51 @@ pub struct NotInCorrectRound { #[derive(Debug)] pub struct OtherReferencesActive; +pub struct WritePermission<'a, T> { + slot: &'a mut MaybeUninit>, + session_id: usize, + t: Arc, +} + +impl WritePermission<'_, T> { + pub fn value(&self) -> &T { + &self.t + } + + pub fn session_id(&self) -> usize { + self.session_id + } +} + +impl Drop for WritePermission<'_, T> { + fn drop(&mut self) { + self.slot.write(self.t.clone()); + } +} + impl Musig2SessionManager where SecondRound: Musig2SignerSecondRound, FirstRound: Musig2SignerFirstRound, { - pub fn new_session(&mut self, first_round: FirstRound) -> Option { - let next_empty = self.tracker.find_next_empty_slot()?; - if next_empty <= self.first_rounds.len() { + pub fn new_session( + &mut self, + first_round: FirstRound, + ) -> Result, OutOfRange> { + let next_empty = self.tracker.find_next_empty_slot().ok_or(OutOfRange)?; + let slot = if next_empty <= self.first_rounds.len() { // we're replacing an existing session - self.first_rounds[next_empty] = MaybeUninit::new(first_round.into()); + self.first_rounds.get_mut(next_empty).unwrap() } else { // we're not replacing any existing session, so we need to grow - self.first_rounds.push(MaybeUninit::new(first_round.into())); - } - return Some(next_empty); + self.first_rounds.push(MaybeUninit::uninit()); + self.first_rounds.last_mut().unwrap() + }; + Ok(WritePermission { + slot, + session_id: next_empty, + t: first_round.into(), + }) } #[inline] diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index accfa278..10f598c7 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -6,14 +6,18 @@ edition = "2021" [dependencies] bitcoin.workspace = true blake3.workspace = true -musig2.workspace = true +musig2 = { path = "../musig2" } +parking_lot.workspace = true +rand.workspace = true rcgen = "0.13.2" +rkyv.workspace = true rustls-pemfile = "2.2.0" secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } secret-service-server = { version = "0.1.0", path = "../secret-service-server" } serde.workspace = true sled = "0.34.7" strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } +terrors.workspace = true tokio.workspace = true toml = "0.8.19" diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs new file mode 100644 index 00000000..0c0da150 --- /dev/null +++ b/crates/secret-service/src/disk/mod.rs @@ -0,0 +1,5 @@ +pub mod musig2; +pub mod operator; +pub mod p2p; +pub mod stakechain; +pub mod wots; diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/secret-service/src/disk/operator.rs b/crates/secret-service/src/disk/operator.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/secret-service/src/disk/p2p.rs b/crates/secret-service/src/disk/p2p.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/secret-service/src/disk/wots.rs b/crates/secret-service/src/disk/wots.rs new file mode 100644 index 00000000..e69de29b diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 390550ed..7d8109fc 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,16 +1,28 @@ // use secret_service_server::rustls::ServerConfig; +pub mod disk; + use std::{ + cell::RefCell, env::args, - fs, future::Future, + io, net::SocketAddr, path::{Path, PathBuf}, str::FromStr, }; +use musig2::{ + errors::{RoundContributionError, RoundFinalizeError}, + secp256k1::PublicKey, + FirstRound, KeyAggContext, LiftedSignature, SecondRound, +}; +use parking_lot::Mutex; +use rand::{thread_rng, Rng}; +use rkyv::rancor; use secret_service_proto::v1::traits::{ - Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, Origin, SecretService, Server, + Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, + Origin, SecretService, Server, }; use secret_service_server::{ run_server, @@ -18,18 +30,15 @@ use secret_service_server::{ pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, ServerConfig, }, - Config, + Config, RoundPersister, }; +use sled::{Db, Tree}; +use terrors::OneOf; +use tokio::{fs, task::spawn_blocking}; use tracing::info; #[tokio::main] async fn main() { - // let config = ServerConfig::builder() - // .with_client_cert_verifier(client_cert_verifier) - // let config = Config { - // addr: SocketAddr::V4(()) - // } - // run_server(c, service) let config_path = PathBuf::from_str(&args().nth(1).unwrap_or_else(|| "config.toml".to_string())) .expect("valid config path"); @@ -37,8 +46,12 @@ async fn main() { let text = std::fs::read_to_string(&config_path).expect("read config file"); let conf: TomlConfig = toml::from_str(&text).expect("valid toml"); - let (certs, key) = if let (Some(key_path), Some(cert_path)) = (&conf.key, &conf.cert) { - let key = fs::read(key_path).expect("readable key"); + let (certs, key) = if let Some(TlsConfig { + cert: Some(ref crt_path), + key: Some(ref key_path), + }) = conf.tls + { + let key = fs::read(key_path).await.expect("readable key"); let key = if key_path.extension().is_some_and(|x| x == "der") { PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) } else { @@ -46,8 +59,8 @@ async fn main() { .expect("valid PEM-encoded private key") .expect("non-empty private key") }; - let cert_chain = fs::read(cert_path).expect("readable certificate"); - let cert_chain = if cert_path.extension().is_some_and(|x| x == "der") { + let cert_chain = fs::read(crt_path).await.expect("readable certificate"); + let cert_chain = if crt_path.extension().is_some_and(|x| x == "der") { vec![CertificateDer::from(cert_chain)] } else { rustls_pemfile::certs(&mut &*cert_chain) @@ -70,27 +83,100 @@ async fn main() { .expect("valid rustls config"); let config = Config { - addr: conf.addr, + addr: conf.transport.addr, tls_config: tls, - connection_limit: conf.conn_limit, + connection_limit: conf.transport.conn_limit, }; - run_server(config, Service.into()).unwrap().await; + let service = Service::load_from_seed_and_db( + &conf + .seed + .unwrap_or(PathBuf::from_str("seed").expect("valid path")), + conf.db + .unwrap_or(PathBuf::from_str("db").expect("valid path")), + ) + .await + .expect("good service"); + + run_server(config, service.into()).unwrap().await; } #[derive(serde::Deserialize)] struct TomlConfig { + tls: Option, + transport: TransportConfig, + seed: Option, + db: Option, +} + +#[derive(serde::Deserialize)] +struct TransportConfig { addr: SocketAddr, conn_limit: Option, +} + +#[derive(serde::Deserialize)] +struct TlsConfig { cert: Option, key: Option, } -struct Service; +struct Service { + seed: [u8; 32], + db: Db, +} + +impl Service { + async fn load_from_seed_and_db(seed_path: &Path, db_path: PathBuf) -> io::Result { + let mut seed = [0; 32]; + + if let Some(parent) = seed_path.parent() { + fs::create_dir_all(parent).await?; + } + + match fs::read(seed_path).await { + Ok(vec) => seed.copy_from_slice(&vec), + Err(e) if e.kind() == io::ErrorKind::NotFound => { + let mut rng = rand::thread_rng(); + rng.fill(&mut seed); + fs::write(seed_path, &seed).await?; + } + Err(e) => return Err(e), + }; + + let db = spawn_blocking(move || sled::open(db_path)) + .await + .expect("thread ok")?; + Ok(Self { seed, db }) + } +} + +struct ServerFirstRound { + session_id: Musig2SessionId, + tree: Tree, + first_round: FirstRound, + ordered_public_keys: Vec, +} + +impl RoundPersister for ServerFirstRound { + type Error = OneOf<(rancor::Error, sled::Error)>; -struct FirstRound; + fn persist( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send { + async move { + let bytes = rkyv::to_bytes::(&self.first_round).map_err(OneOf::new)?; + self.tree + .insert(&session_id.to_be_bytes(), bytes.as_ref()) + .map_err(OneOf::new)?; + self.tree.flush_async().await.map_err(OneOf::new)?; + Ok(()) + } + } +} -impl Musig2SignerFirstRound for FirstRound { +impl Musig2SignerFirstRound for ServerFirstRound { fn our_nonce( &self, ) -> impl Future::Container> + Send { @@ -99,8 +185,7 @@ impl Musig2SignerFirstRound for FirstRound { fn holdouts( &self, - ) -> impl Future::Container>> + Send - { + ) -> impl Future::Container>> + Send { async move { todo!() } } @@ -110,11 +195,10 @@ impl Musig2SignerFirstRound for FirstRound { fn receive_pub_nonce( &self, - pubkey: musig2::secp256k1::PublicKey, + pubkey: PublicKey, pubnonce: musig2::PubNonce, - ) -> impl Future< - Output = ::Container>, - > + Send { + ) -> impl Future::Container>> + Send + { async move { todo!() } } @@ -122,75 +206,108 @@ impl Musig2SignerFirstRound for FirstRound { self, hash: [u8; 32], ) -> impl Future< - Output = ::Container< - Result, - >, + Output = ::Container>, > + Send { async move { todo!() } } } -struct SecondRound; +struct ServerSecondRound { + session_id: Musig2SessionId, + tree: Tree, + second_round: Mutex>, + ordered_public_keys: Mutex>, +} + +impl RoundPersister for ServerSecondRound { + type Error = OneOf<(rancor::Error, sled::Error)>; + + fn persist( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send { + async move { + let bytes = + rkyv::to_bytes::(&*self.second_round.lock()).map_err(OneOf::new)?; + self.tree + .insert(&session_id.to_be_bytes(), bytes.as_ref()) + .map_err(OneOf::new)?; + self.tree.flush_async().await.map_err(OneOf::new)?; + Ok(()) + } + } +} -impl Musig2SignerSecondRound for SecondRound { +impl Musig2SignerSecondRound for ServerSecondRound { fn agg_nonce( &self, ) -> impl Future::Container> + Send { - async move { todo!() } + async move { self.second_round.lock().aggregated_nonce().clone() } } fn holdouts( &self, - ) -> impl Future::Container>> + Send - { - async move { todo!() } + ) -> impl Future::Container>> + Send { + async move { + let ordered_public_keys = self.ordered_public_keys.lock(); + self.second_round + .lock() + .holdouts() + .into_iter() + .map(|idx| ordered_public_keys[*idx]) + .collect() + } } fn our_signature( &self, ) -> impl Future::Container> + Send { - async move { todo!() } + async move { self.second_round.lock().our_signature() } } fn is_complete(&self) -> impl Future::Container> + Send { - async move { todo!() } + async move { self.second_round.lock().is_complete() } } fn receive_signature( &self, - pubkey: musig2::secp256k1::PublicKey, + pubkey: PublicKey, signature: musig2::PartialSignature, - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { todo!() } + ) -> impl Future::Container>> + Send + { + async move { + let signer_idx = self + .ordered_public_keys + .lock() + .iter() + .position(|x| x == &pubkey) + .ok_or(RoundContributionError::out_of_range(0, 0))?; + self.second_round + .lock() + .receive_signature(signer_idx, signature) + } } fn finalize( self, ) -> impl Future< - Output = ::Container< - Result, - >, + Output = ::Container>, > + Send { - async move { todo!() } + async move { self.second_round.into_inner().finalize() } } } struct Operator; impl OperatorSigner for Operator { - fn sign_psbt( - &self, - psbt: bitcoin::Psbt, - ) -> impl Future> + Send { + fn sign_psbt(&self, psbt: bitcoin::Psbt) -> impl Future + Send { async move { todo!() } } } struct P2PSigner; -impl SecretService for Service { +impl SecretService for Service { type OperatorSigner = Operator; type P2PSigner; @@ -215,3 +332,30 @@ impl SecretService for Service { todo!() } } + +struct Ms2Signer { + tree: Tree, +} + +impl Musig2Signer for Ms2Signer { + fn new_session( + &self, + public_keys: Vec, + ) -> impl Future + Send { + async move { + let nonce_seed = thread_rng().gen::<[u8; 32]>(); + let first_round = FirstRound::new( + KeyAggContext::new(public_keys), + nonce_seed, + signer_index, + spices, + ); + ServerFirstRound { + session_id, + tree: self.tree.clone(), + first_round, + ordered_public_keys: public_keys, + } + } + } +} From 64a2d02fa635c1a7698dc358ac670455dc827222 Mon Sep 17 00:00:00 2001 From: Azz Date: Tue, 11 Feb 2025 21:23:18 +0000 Subject: [PATCH 09/30] I HAVE PLEASED `rustc`!!! --- Cargo.toml | 9 - crates/musig2/src/lib.rs | 2 +- crates/secret-service-client/src/lib.rs | 90 ++++-- crates/secret-service-proto/src/v1/traits.rs | 47 +-- crates/secret-service-proto/src/v1/wire.rs | 29 +- crates/secret-service-server/src/lib.rs | 227 ++++++++------ crates/secret-service-server/src/ms2sm.rs | 39 ++- crates/secret-service/Cargo.toml | 2 + crates/secret-service/src/config.rs | 21 ++ crates/secret-service/src/disk/mod.rs | 90 ++++++ crates/secret-service/src/disk/musig2.rs | 302 +++++++++++++++++++ crates/secret-service/src/disk/operator.rs | 32 ++ crates/secret-service/src/disk/p2p.rs | 29 ++ crates/secret-service/src/disk/stakechain.rs | 27 ++ crates/secret-service/src/disk/wots.rs | 34 +++ crates/secret-service/src/main.rs | 294 +----------------- 16 files changed, 817 insertions(+), 457 deletions(-) create mode 100644 crates/secret-service/src/config.rs diff --git a/Cargo.toml b/Cargo.toml index f4cde209..989b2a74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,13 +67,9 @@ zkaleido-sp1-adapter = { git = "https://github.com/alpenlabs/zkaleido", tag = "v anyhow = "1.0.95" arbitrary = { version = "1.4.1", features = ["derive"] } ark-bn254 = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } -ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives/" } ark-ec = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } ark-ff = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } ark-groth16 = { git = "https://github.com/arkworks-rs/groth16" } -ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std/" } -ark-relations = { git = "https://github.com/arkworks-rs/snark/" } -ark-std = { git = "https://github.com/arkworks-rs/std/" } async-trait = "0.1.81" base64 = "0.22.1" bincode = "1.3.3" @@ -86,16 +82,13 @@ borsh = { version = "1.5.0", features = ["derive"] } chrono = "0.4.38" clap = { version = "4.5.20", features = ["cargo", "derive", "env"] } corepc-node = { version = "0.5.0", features = ["28_0", "download"] } -dotenvy = "0.15.7" esplora-client = { git = "https://github.com/BitVM/rust-esplora-client", default-features = false, features = [ "blocking-https-rustls", "async-https-rustls", ] } -ethnum = "1.5.0" futures = "0.3.31" hex = { version = "0.4", features = ["serde"] } jsonrpsee = "0.24.7" -jsonrpsee-types = "0.24.7" kanal = "0.1.0-pre8" musig2 = { version = "0.1.0", features = [ "serde", @@ -118,7 +111,6 @@ serde_json = { version = "1.0", default-features = false, features = [ "alloc", "raw_value", ] } -serde_with = "3.12.0" sha2 = "0.10" sqlx = { version = "0.8.2", features = [ "sqlite", @@ -128,7 +120,6 @@ sqlx = { version = "0.8.2", features = [ "derive", "migrate", ] } -tempfile = "3.10.1" terrors = "0.3.2" thiserror = "2.0.3" tokio = { version = "1.37", features = ["full"] } diff --git a/crates/musig2/src/lib.rs b/crates/musig2/src/lib.rs index 21b33d3b..7c20303b 100644 --- a/crates/musig2/src/lib.rs +++ b/crates/musig2/src/lib.rs @@ -13,7 +13,6 @@ mod bip340; mod key_agg; mod key_sort; mod nonces; -mod rkyv_wrappers; mod rounds; mod sig_agg; mod signature; @@ -33,6 +32,7 @@ pub mod deterministic; pub mod errors; pub mod tagged_hashes; +pub mod rkyv_wrappers; pub use binary_encoding::*; pub use bip340::{sign_solo, verify_single}; pub use key_agg::*; diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index bb674258..33d0f3d7 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -6,11 +6,11 @@ use std::{ time::Duration, }; -use bitcoin::Psbt; +use bitcoin::{hashes::Hash, Txid}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::{Error, PublicKey}, - AggNonce, LiftedSignature, PubNonce, + secp256k1::{schnorr::Signature, Error, PublicKey}, + AggNonce, KeyAggContext, LiftedSignature, PubNonce, }; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, @@ -22,7 +22,7 @@ use secret_service_proto::{ traits::{ Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, - StakeChainPreimages, WotsSigner, + SignerIdxOutOfBounds, StakeChainPreimages, WotsSigner, }, wire::{ClientMessage, ServerMessage}, }, @@ -189,7 +189,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { } fn receive_pub_nonce( - &self, + &mut self, pubkey: PublicKey, pubnonce: PubNonce, ) -> impl Future::Container>> + Send @@ -303,7 +303,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { } fn receive_signature( - &self, + &mut self, pubkey: PublicKey, signature: musig2::PartialSignature, ) -> impl Future::Container>> + Send @@ -354,19 +354,34 @@ struct OperatorClient { } impl OperatorSigner for OperatorClient { - fn sign_psbt( + fn sign( &self, - psbt: Psbt, - ) -> impl Future::Container> + Send { + digest: &[u8; 32], + ) -> impl Future::Container> + Send { async move { - let msg = ClientMessage::OperatorSignPsbt { - psbt: psbt.serialize(), + let msg = ClientMessage::OperatorSign { + digest: digest.clone(), }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::OperatorSignPsbt { psbt } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Psbt::deserialize(&psbt).map_err(|_| ClientError::BadData) + match res { + ServerMessage::OperatorSignPsbt { sig } => { + Signature::from_slice(&sig).map_err(|_| ClientError::BadData) + } + _ => Err(ClientError::ProtocolError(res)), + } + } + } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::OperatorPubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + match res { + ServerMessage::OperatorPubkey { pubkey } => { + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) + } + _ => Err(ClientError::ProtocolError(res)), + } } } } @@ -377,28 +392,30 @@ struct P2PClient { } impl P2PSigner for P2PClient { - fn sign_p2p( + fn sign( &self, - hash: [u8; 32], - ) -> impl Future::Container<[u8; 64]>> + Send { + digest: &[u8; 32], + ) -> impl Future::Container> + Send { async move { - let msg = ClientMessage::SignP2P { hash }; + let msg = ClientMessage::P2PSign { + digest: digest.clone(), + }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::SignP2P { sig } = res else { return Err(ClientError::ProtocolError(res)); }; - Ok(sig) + Signature::from_slice(&sig).map_err(|_| ClientError::BadData) } } - fn p2p_pubkey(&self) -> impl Future::Container<[u8; 33]>> + Send { + fn pubkey(&self) -> impl Future::Container> + Send { async move { let msg = ClientMessage::P2PPubkey; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::P2PPubkey { pubkey } = res else { return Err(ClientError::ProtocolError(res)); }; - Ok(pubkey) + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) } } } @@ -411,21 +428,24 @@ struct Musig2Client { impl Musig2Signer for Musig2Client { fn new_session( &self, - public_keys: Vec, - ) -> impl Future> + Send { + ctx: KeyAggContext, + signer_idx: usize, + ) -> impl Future, ClientError>> + Send + { async move { - let msg = ClientMessage::Musig2NewSession { - public_keys: public_keys.into_iter().map(|pk| pk.serialize()).collect(), - }; + let msg = ClientMessage::Musig2NewSession { ctx, signer_idx }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::Musig2NewSession { session_id } = res else { + let ServerMessage::Musig2NewSession(maybe_session_id) = res else { return Err(ClientError::ProtocolError(res)); }; - Ok(Musig2FirstRound { - session_id, - connection: self.conn.clone(), - config: self.config.clone(), + Ok(match maybe_session_id { + Ok(session_id) => Ok(Musig2FirstRound { + session_id, + connection: self.conn.clone(), + config: self.config.clone(), + }), + Err(e) => Err(e), }) } } @@ -440,9 +460,13 @@ impl WotsSigner for WotsClient { fn get_key( &self, index: u64, + txid: Txid, ) -> impl Future::Container<[u8; 64]>> + Send { async move { - let msg = ClientMessage::WotsGetKey { index }; + let msg = ClientMessage::WotsGetKey { + index, + txid: txid.as_raw_hash().to_byte_array(), + }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::WotsGetKey { key } = res else { return Err(ClientError::ProtocolError(res)); diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 8a6a78e7..9844dd03 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -1,13 +1,13 @@ use std::future::Future; -use bitcoin::Psbt; +use bitcoin::Txid; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::PublicKey, - AggNonce, LiftedSignature, PartialSignature, PubNonce, + secp256k1::{schnorr::Signature, PublicKey}, + AggNonce, KeyAggContext, LiftedSignature, PartialSignature, PubNonce, }; use quinn::{ConnectionError, ReadExactError, WriteError}; -use rkyv::rancor; +use rkyv::{rancor, Archive, Deserialize, Serialize}; use super::wire::ServerMessage; @@ -43,32 +43,29 @@ where } pub trait OperatorSigner: Send { - // type OperatorSigningError: Debug - // + Send - // + Clone - // + for<'a> Serialize, rancor::Error>>; - - fn sign_psbt(&self, psbt: Psbt) -> impl Future> + Send; + fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; + fn pubkey(&self) -> impl Future> + Send; } pub trait P2PSigner: Send { - // type P2PSigningError: Debug - // + Send - // + Clone - // + for<'a> Serialize, rancor::Error>>; - - fn sign_p2p(&self, hash: [u8; 32]) -> impl Future> + Send; - - fn p2p_pubkey(&self) -> impl Future> + Send; + fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; + fn pubkey(&self) -> impl Future> + Send; } pub type Musig2SessionId = usize; +#[derive(Debug, Archive, Serialize, Deserialize, Clone)] +pub struct SignerIdxOutOfBounds { + pub index: usize, + pub n_signers: usize, +} + pub trait Musig2Signer: Send + Sync { fn new_session( &self, - public_keys: Vec, - ) -> impl Future> + Send; + ctx: KeyAggContext, + signer_idx: usize, + ) -> impl Future>> + Send; } pub trait Musig2SignerFirstRound: Send + Sync { @@ -79,7 +76,7 @@ pub trait Musig2SignerFirstRound: Send + Sync { fn is_complete(&self) -> impl Future> + Send; fn receive_pub_nonce( - &self, + &mut self, pubkey: PublicKey, pubnonce: PubNonce, ) -> impl Future>> + Send; @@ -100,7 +97,7 @@ pub trait Musig2SignerSecondRound: Send + Sync { fn is_complete(&self) -> impl Future> + Send; fn receive_signature( - &self, + &mut self, pubkey: PublicKey, signature: PartialSignature, ) -> impl Future>> + Send; @@ -111,7 +108,11 @@ pub trait Musig2SignerSecondRound: Send + Sync { } pub trait WotsSigner: Send { - fn get_key(&self, index: u64) -> impl Future> + Send; + fn get_key( + &self, + index: u64, + txid: Txid, + ) -> impl Future> + Send; } pub trait StakeChainPreimages: Send { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 2402082e..f5cec60c 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -1,7 +1,10 @@ -use musig2::errors::{RoundContributionError, RoundFinalizeError}; +use musig2::{ + errors::{RoundContributionError, RoundFinalizeError}, + KeyAggContext, +}; use rkyv::{with::Map, Archive, Deserialize, Serialize}; -use super::traits::Musig2SessionId; +use super::traits::{Musig2SessionId, SignerIdxOutOfBounds}; #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum ServerMessage { @@ -9,7 +12,10 @@ pub enum ServerMessage { OpaqueServerError, OperatorSignPsbt { - psbt: Vec, + sig: [u8; 64], + }, + OperatorPubkey { + pubkey: [u8; 33], }, SignP2P { @@ -19,9 +25,7 @@ pub enum ServerMessage { pubkey: [u8; 33], }, - Musig2NewSession { - session_id: Musig2SessionId, - }, + Musig2NewSession(Result), Musig2FirstRoundOurNonce { our_nonce: [u8; 66], @@ -95,17 +99,19 @@ impl From for Result<[u8; 64], RoundFinalizeError> { #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum ClientMessage { - OperatorSignPsbt { - psbt: Vec, + OperatorSign { + digest: [u8; 32], }, + OperatorPubkey, - SignP2P { - hash: [u8; 32], + P2PSign { + digest: [u8; 32], }, P2PPubkey, Musig2NewSession { - public_keys: Vec<[u8; 33]>, + ctx: KeyAggContext, + signer_idx: usize, }, Musig2FirstRoundOurNonce { @@ -150,6 +156,7 @@ pub enum ClientMessage { WotsGetKey { index: u64, + txid: [u8; 32], }, StakeChainGetPreimage { diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 7803c9d6..0c45f7cb 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -12,15 +12,18 @@ use std::{ task::{Context, Poll}, }; -use bitcoin::{secp256k1::PublicKey, Psbt}; +use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid}; use ms2sm::Musig2SessionManager; -use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; +use musig2::{errors::RoundFinalizeError, KeyAggContext, PartialSignature, PubNonce}; pub use quinn::rustls; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, ConnectionError, Endpoint, Incoming, ReadExactError, RecvStream, SendStream, ServerConfig, }; -use rkyv::rancor::Error; +use rkyv::{ + deserialize, + rancor::{self, Error}, +}; use secret_service_proto::{ v1::{ traits::{ @@ -56,16 +59,16 @@ impl Future for ServerHandle { } } -pub fn run_server( +pub fn run_server( c: Config, service: Arc, + round_persister: Arc, ) -> Result> where - FirstRound: Musig2SignerFirstRound + 'static + RoundPersister, - SecondRound: Musig2SignerSecondRound + 'static + RoundPersister, + FirstRound: Musig2SignerFirstRound + 'static, + SecondRound: Musig2SignerSecondRound + 'static, Service: SecretService + Sync + 'static, - >::Musig2Signer: - Musig2RoundRecovery, + Persister: RoundPersister + Send + Sync + 'static, { let quic_server_config = ServerConfig::with_crypto(Arc::new( QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, @@ -86,7 +89,13 @@ where incoming.refuse(); } else { tokio::spawn( - conn_handler(incoming, service.clone(), musig2_sm.clone()).instrument(span), + conn_handler( + incoming, + service.clone(), + round_persister.clone(), + musig2_sm.clone(), + ) + .instrument(span), ); } } @@ -95,16 +104,16 @@ where Ok(ServerHandle { main: handle }) } -async fn conn_handler( +async fn conn_handler( incoming: Incoming, service: Arc, + round_persister: Arc, musig2_sm: Arc>>, ) where - FirstRound: Musig2SignerFirstRound + 'static + RoundPersister, - SecondRound: Musig2SignerSecondRound + 'static + RoundPersister, + FirstRound: Musig2SignerFirstRound + 'static, + SecondRound: Musig2SignerSecondRound + 'static, Service: SecretService + Sync + 'static, - >::Musig2Signer: - Musig2RoundRecovery, + Persister: RoundPersister + Send + Sync + 'static, { let conn = match incoming.await { Ok(conn) => conn, @@ -133,8 +142,13 @@ async fn conn_handler( request_manager( tx, tokio::spawn( - request_handler(rx, service.clone(), musig2_sm.clone()) - .instrument(handler_span), + request_handler( + rx, + service.clone(), + round_persister.clone(), + musig2_sm.clone(), + ) + .instrument(handler_span), ), ) .instrument(manager_span), @@ -171,17 +185,17 @@ async fn request_manager( } } -async fn request_handler( +async fn request_handler( mut rx: RecvStream, service: Arc, + round_persister: Arc, musig2_sm: Arc>>, ) -> Result where - FirstRound: Musig2SignerFirstRound + RoundPersister, - SecondRound: Musig2SignerSecondRound + RoundPersister, + FirstRound: Musig2SignerFirstRound, + SecondRound: Musig2SignerSecondRound, Service: SecretService, - >::Musig2Signer: - Musig2RoundRecovery, + Persister: RoundPersister + Send + Sync + 'static, { let len_to_read = { let mut buf = [0; size_of::()]; @@ -196,54 +210,61 @@ where Ok(match msg { // this would be a separate function but tokio would start whining because !Sync ArchivedVersionedClientMessage::V1(req) => match req { - ArchivedClientMessage::OperatorSignPsbt { psbt } => { - let psbt = Psbt::deserialize(&psbt).unwrap(); - let psbt = service.operator_signer().sign_psbt(psbt).await; + ArchivedClientMessage::OperatorSign { digest } => { + let sig = service.operator_signer().sign(digest).await; ServerMessage::OperatorSignPsbt { - psbt: psbt.serialize(), + sig: sig.serialize(), } } - ArchivedClientMessage::SignP2P { hash } => { - let sig = service.p2p_signer().sign_p2p(*hash).await; - ServerMessage::SignP2P { sig } + ArchivedClientMessage::OperatorPubkey => { + let pubkey = service.operator_signer().pubkey().await; + ServerMessage::OperatorPubkey { + pubkey: pubkey.serialize(), + } + } + + ArchivedClientMessage::P2PSign { digest } => { + let sig = service.p2p_signer().sign(digest).await; + ServerMessage::SignP2P { + sig: sig.serialize(), + } } ArchivedClientMessage::P2PPubkey => { - let pubkey = service.p2p_signer().p2p_pubkey().await; - ServerMessage::P2PPubkey { pubkey } + let pubkey = service.p2p_signer().pubkey().await; + ServerMessage::P2PPubkey { + pubkey: pubkey.serialize(), + } } - ArchivedClientMessage::Musig2NewSession { public_keys } => 'block: { + ArchivedClientMessage::Musig2NewSession { ctx, signer_idx } => 'block: { let signer = service.musig2_signer(); - let public_keys: Result, _> = public_keys - .iter() - .map(AsRef::<[u8]>::as_ref) - .map(PublicKey::from_slice) - .collect(); - - let Ok(mut public_keys) = public_keys else { + let Ok(ctx) = deserialize::(ctx) else { break 'block ServerMessage::InvalidClientMessage; }; - - // enforce sorting at the protocol level - public_keys.sort(); - - let first_round = signer.new_session(public_keys).await; + let first_round = match signer + .new_session(ctx, signer_idx.to_native() as usize) + .await + { + Ok(fr) => fr, + Err(e) => break 'block ServerMessage::Musig2NewSession(Err(e)), + }; let mut sm = musig2_sm.lock().await; let Ok(write_perm) = sm.new_session(first_round) else { break 'block ServerMessage::OpaqueServerError; }; - if let Err(e) = write_perm.value().persist(write_perm.session_id()).await { + if let Err(e) = round_persister + .persist_first_round(write_perm.session_id(), &*write_perm.value().await) + .await + { error!("failed to persist first round: {e:?}"); break 'block ServerMessage::OpaqueServerError; } - ServerMessage::Musig2NewSession { - session_id: write_perm.session_id(), - } + ServerMessage::Musig2NewSession(Ok(write_perm.session_id())) } ArchivedClientMessage::Musig2FirstRoundOurNonce { session_id } => { let r = musig2_sm @@ -252,7 +273,7 @@ where .first_round(session_id.to_native() as usize); match r { Ok(Some(first_round)) => { - let our_nonce = first_round.our_nonce().await.serialize(); + let our_nonce = first_round.lock().await.our_nonce().await.serialize(); ServerMessage::Musig2FirstRoundOurNonce { our_nonce } } _ => ServerMessage::InvalidClientMessage, @@ -266,6 +287,8 @@ where match r { Ok(Some(first_round)) => ServerMessage::Musig2FirstRoundHoldouts { pubkeys: first_round + .lock() + .await .holdouts() .await .iter() @@ -282,7 +305,7 @@ where .first_round(session_id.to_native() as usize); match r { Ok(Some(first_round)) => ServerMessage::Musig2FirstRoundIsComplete { - complete: first_round.is_complete().await, + complete: first_round.lock().await.is_complete().await, }, _ => ServerMessage::InvalidClientMessage, } @@ -292,26 +315,29 @@ where pubkey, pubnonce, } => { - let r = musig2_sm - .lock() - .await - .first_round(session_id.to_native() as usize); + let session_id = session_id.to_native() as usize; + let r = musig2_sm.lock().await.first_round(session_id); let pubkey = PublicKey::from_slice(pubkey); let pubnonce = PubNonce::from_bytes(pubnonce); match (r, pubkey, pubnonce) { (Ok(Some(first_round)), Ok(pubkey), Ok(pubnonce)) => { - let r = first_round.receive_pub_nonce(pubkey, pubnonce).await; - ServerMessage::Musig2FirstRoundReceivePubNonce(r.err()) + let mut fr = first_round.lock().await; + let r = fr.receive_pub_nonce(pubkey, pubnonce).await; + if let Err(e) = round_persister.persist_first_round(session_id, &*fr).await + { + error!("failed to persist first round: {e:?}"); + ServerMessage::OpaqueServerError + } else { + ServerMessage::Musig2FirstRoundReceivePubNonce(r.err()) + } } _ => ServerMessage::InvalidClientMessage, } } - ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => { - let r = musig2_sm - .lock() - .await - .transition_first_to_second_round(session_id.to_native() as usize, *hash) - .await; + ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => 'block: { + let session_id = session_id.to_native() as usize; + let mut sm = musig2_sm.lock().await; + let r = sm.transition_first_to_second_round(session_id, *hash).await; if let Err(e) = r { use terrors::E3::*; @@ -324,6 +350,19 @@ where }, } } else { + let mutex = sm + .second_round(session_id) + .expect("in range") + .expect("transitioned to second round"); + let sr = mutex.lock().await; + if let Err(e) = round_persister.persist_second_round(session_id, &sr).await { + error!("failed to persist second round: {e:?}"); + break 'block ServerMessage::OpaqueServerError; + } + if let Err(e) = round_persister.delete_first_round(session_id).await { + error!("failed to delete first round: {e:?}"); + break 'block ServerMessage::OpaqueServerError; + } ServerMessage::Musig2FirstRoundFinalize(None) } } @@ -336,7 +375,7 @@ where match sr { Ok(Some(sr)) => ServerMessage::Musig2SecondRoundAggNonce { - nonce: sr.agg_nonce().await.serialize(), + nonce: sr.lock().await.agg_nonce().await.serialize(), }, _ => ServerMessage::InvalidClientMessage, } @@ -350,6 +389,8 @@ where match sr { Ok(Some(sr)) => ServerMessage::Musig2SecondRoundHoldouts { pubkeys: sr + .lock() + .await .holdouts() .await .iter() @@ -367,7 +408,7 @@ where match sr { Ok(Some(sr)) => ServerMessage::Musig2SecondRoundOurSignature { - sig: sr.our_signature().await.serialize(), + sig: sr.lock().await.our_signature().await.serialize(), }, _ => ServerMessage::InvalidClientMessage, } @@ -380,7 +421,7 @@ where match sr { Ok(Some(sr)) => ServerMessage::Musig2SecondRoundIsComplete { - complete: sr.is_complete().await, + complete: sr.lock().await.is_complete().await, }, _ => ServerMessage::InvalidClientMessage, } @@ -390,16 +431,21 @@ where pubkey, signature, } => { - let sr = musig2_sm - .lock() - .await - .second_round(session_id.to_native() as usize); + let session_id = session_id.to_native() as usize; + let sr = musig2_sm.lock().await.second_round(session_id); let pubkey = PublicKey::from_slice(pubkey); let signature = PartialSignature::from_slice(signature); match (sr, pubkey, signature) { (Ok(Some(sr)), Ok(pubkey), Ok(signature)) => { + let mut sr = sr.lock().await; let r = sr.receive_signature(pubkey, signature).await; - ServerMessage::Musig2SecondRoundReceiveSignature(r.err()) + if let Err(e) = round_persister.persist_second_round(session_id, &sr).await + { + error!("failed to persist second round: {e:?}"); + ServerMessage::OpaqueServerError + } else { + ServerMessage::Musig2SecondRoundReceiveSignature(r.err()) + } } _ => ServerMessage::InvalidClientMessage, } @@ -410,20 +456,16 @@ where .await .finalize_second_round(session_id.to_native() as usize) .await; - match r { + match r.map_err(|e| e.narrow::()) { Ok(sig) => ServerMessage::Musig2SecondRoundFinalize(Ok(sig.serialize()).into()), - Err(e) => { - if let Ok(e) = e.narrow::() { - ServerMessage::Musig2SecondRoundFinalize(Err(e).into()) - } else { - ServerMessage::InvalidClientMessage - } - } + Err(Ok(e)) => ServerMessage::Musig2SecondRoundFinalize(Err(e).into()), + Err(Err(_e)) => ServerMessage::InvalidClientMessage, } } - ArchivedClientMessage::WotsGetKey { index } => { - let key = service.wots_signer().get_key(index.into()).await; + ArchivedClientMessage::WotsGetKey { index, txid } => { + let txid = Txid::from_slice(txid).expect("correct length"); + let key = service.wots_signer().get_key(index.into(), txid).await; ServerMessage::WotsGetKey { key } } @@ -435,17 +477,34 @@ where }) } -pub trait RoundPersister { +pub trait RoundPersister +where + FirstRound: Musig2SignerFirstRound, + SecondRound: Musig2SignerSecondRound, +{ type Error: Debug; - fn persist( + fn persist_first_round( &self, session_id: Musig2SessionId, + first_round: &FirstRound, ) -> impl Future> + Send; -} -pub trait Musig2RoundRecovery { - type Error: Debug; + fn persist_second_round( + &self, + session_id: Musig2SessionId, + second_round: &SecondRound, + ) -> impl Future> + Send; + + fn delete_first_round( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send; + + fn delete_second_round( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send; fn load_first_rounds( &self, diff --git a/crates/secret-service-server/src/ms2sm.rs b/crates/secret-service-server/src/ms2sm.rs index 31babc21..3a042f8f 100644 --- a/crates/secret-service-server/src/ms2sm.rs +++ b/crates/secret-service-server/src/ms2sm.rs @@ -3,6 +3,7 @@ use std::{mem::MaybeUninit, ptr, sync::Arc}; use musig2::{errors::RoundFinalizeError, LiftedSignature}; use secret_service_proto::v1::traits::{Musig2SignerFirstRound, Musig2SignerSecondRound, Server}; use terrors::OneOf; +use tokio::sync::{Mutex, MutexGuard}; use crate::bool_arr::DoubleBoolArray; @@ -17,11 +18,11 @@ where /// Used to store first rounds of musig2 server instances. This is a Vec /// because we don't know how big FirstRound may be in memory so we will /// heap allocate and try keep this to a minimum - first_rounds: Vec>>, + first_rounds: Vec>>>, /// Used to store second rounds of musig2 server instances. This is a Vec /// because we don't know how big SecondRound may be in memory so we will /// heap allocate and try keep this to a minimum - second_rounds: Vec>>, + second_rounds: Vec>>>, } impl Default @@ -52,14 +53,14 @@ pub struct NotInCorrectRound { pub struct OtherReferencesActive; pub struct WritePermission<'a, T> { - slot: &'a mut MaybeUninit>, + slot: &'a mut MaybeUninit>>, session_id: usize, - t: Arc, + t: Arc>, } impl WritePermission<'_, T> { - pub fn value(&self) -> &T { - &self.t + pub async fn value(&self) -> MutexGuard<'_, T> { + self.t.lock().await } pub fn session_id(&self) -> usize { @@ -94,7 +95,7 @@ where Ok(WritePermission { slot, session_id: next_empty, - t: first_round.into(), + t: Arc::new(first_round.into()), }) } @@ -135,8 +136,12 @@ where return Err(OneOf::new(OtherReferencesActive)); } }; - let second_round = first_round.finalize(hash).await.map_err(OneOf::new)?; - self.second_rounds[session_id] = MaybeUninit::new(second_round.into()); + let second_round = first_round + .into_inner() + .finalize(hash) + .await + .map_err(OneOf::new)?; + self.second_rounds[session_id] = MaybeUninit::new(Arc::new(second_round.into())); self.tracker.set(session_id, SlotState::SecondRound); Ok(()) } @@ -176,7 +181,11 @@ where } }; self.tracker.set(session_id, SlotState::Empty); - Ok(second_round.finalize().await.map_err(OneOf::new)?) + Ok(second_round + .into_inner() + .finalize() + .await + .map_err(OneOf::new)?) } slot_state => Err(OneOf::new(NotInCorrectRound { wanted: SlotState::SecondRound, @@ -185,7 +194,10 @@ where } } - pub fn first_round(&self, session_id: usize) -> Result>, OutOfRange> { + pub fn first_round( + &self, + session_id: usize, + ) -> Result>>, OutOfRange> { match self.slot_state(session_id)? { SlotState::FirstRound => { let first_round = unsafe { self.first_rounds[session_id].assume_init_ref() }; @@ -195,7 +207,10 @@ where } } - pub fn second_round(&self, session_id: usize) -> Result>, OutOfRange> { + pub fn second_round( + &self, + session_id: usize, + ) -> Result>>, OutOfRange> { match self.slot_state(session_id)? { SlotState::SecondRound => { let second_round = unsafe { self.second_rounds[session_id].assume_init_ref() }; diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index 10f598c7..abb34aaf 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] bitcoin.workspace = true blake3.workspace = true +hkdf = "0.12.4" musig2 = { path = "../musig2" } parking_lot.workspace = true rand.workspace = true @@ -15,6 +16,7 @@ rustls-pemfile = "2.2.0" secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } secret-service-server = { version = "0.1.0", path = "../secret-service-server" } serde.workspace = true +sha2.workspace = true sled = "0.34.7" strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } terrors.workspace = true diff --git a/crates/secret-service/src/config.rs b/crates/secret-service/src/config.rs new file mode 100644 index 00000000..00b3373d --- /dev/null +++ b/crates/secret-service/src/config.rs @@ -0,0 +1,21 @@ +use std::{net::SocketAddr, path::PathBuf}; + +#[derive(serde::Deserialize)] +pub struct TomlConfig { + pub tls: Option, + pub transport: TransportConfig, + pub seed: Option, + pub db: Option, +} + +#[derive(serde::Deserialize)] +pub struct TransportConfig { + pub addr: SocketAddr, + pub conn_limit: Option, +} + +#[derive(serde::Deserialize)] +pub struct TlsConfig { + pub cert: Option, + pub key: Option, +} diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 0c0da150..3e668a89 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -1,5 +1,95 @@ +use std::path::{Path, PathBuf}; + +use bitcoin::{bip32::Xpriv, Network}; +use musig2::{Ms2Signer, ServerFirstRound, ServerSecondRound, SledRoundPersist}; +use operator::Operator; +use p2p::ServerP2PSigner; +use rand::Rng; +use secret_service_proto::v1::traits::{SecretService, Server}; +use sled::Db; +use stakechain::StakeChain; +use strata_key_derivation::operator::OperatorKeys; +use tokio::{fs, io, task::spawn_blocking}; +use wots::SeededWotsSigner; + pub mod musig2; pub mod operator; pub mod p2p; pub mod stakechain; pub mod wots; + +pub struct Service { + keys: OperatorKeys, + db: Db, +} + +const NETWORK: Network = Network::Signet; + +impl Service { + pub async fn load_from_seed_and_db(seed_path: &Path, db_path: PathBuf) -> io::Result { + let mut seed = [0; 32]; + + if let Some(parent) = seed_path.parent() { + fs::create_dir_all(parent).await?; + } + + match fs::read(seed_path).await { + Ok(vec) => seed.copy_from_slice(&vec), + Err(e) if e.kind() == io::ErrorKind::NotFound => { + let mut rng = rand::thread_rng(); + rng.fill(&mut seed); + fs::write(seed_path, &seed).await?; + } + Err(e) => return Err(e), + }; + + let db = spawn_blocking(move || sled::open(db_path)) + .await + .expect("thread ok")?; + + let keys = OperatorKeys::new(&Xpriv::new_master(NETWORK, &seed).expect("valid xpriv")) + .expect("valid keychain"); + Ok(Self { keys, db }) + } + + pub fn round_persister(&self) -> io::Result { + Ok(SledRoundPersist::new( + self.db.open_tree(b"musig2_first_rounds")?, + self.db.open_tree(b"musig2_second_rounds")?, + )) + } +} + +impl SecretService for Service { + type OperatorSigner = Operator; + + type P2PSigner = ServerP2PSigner; + + type Musig2Signer = Ms2Signer; + + type WotsSigner = SeededWotsSigner; + + type StakeChain = StakeChain; + + fn operator_signer(&self) -> Self::OperatorSigner { + Operator::new(self.keys.wallet_xpriv().private_key) + } + + fn p2p_signer(&self) -> Self::P2PSigner { + ServerP2PSigner::new(self.keys.message_xpriv().private_key) + } + + fn musig2_signer(&self) -> Self::Musig2Signer { + Ms2Signer::new(self.keys.wallet_xpriv().private_key) + } + + fn wots_signer(&self) -> Self::WotsSigner { + let seed = self.keys.base_xpriv().private_key.secret_bytes(); + SeededWotsSigner::new(seed) + } + + fn stake_chain(&self) -> Self::StakeChain { + let seed = self.keys.base_xpriv().private_key.secret_bytes(); + StakeChain::new(seed) + } +} diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index e69de29b..18eccb8a 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -0,0 +1,302 @@ +use std::future::Future; + +use musig2::{ + errors::{RoundContributionError, RoundFinalizeError}, + secp256k1::{PublicKey, SecretKey}, + FirstRound, KeyAggContext, LiftedSignature, SecNonceSpices, SecondRound, +}; +use rand::{thread_rng, Rng}; +use rkyv::{rancor, with::Map, Archive, Deserialize, Serialize}; +use secret_service_proto::v1::traits::{ + Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, Server, + SignerIdxOutOfBounds, +}; +use secret_service_server::RoundPersister; +use sled::Tree; +use terrors::OneOf; + +pub struct Ms2Signer { + key: SecretKey, +} + +impl Ms2Signer { + pub fn new(key: SecretKey) -> Self { + Self { key } + } +} + +impl Musig2Signer for Ms2Signer { + fn new_session( + &self, + ctx: KeyAggContext, + signer_idx: usize, + ) -> impl Future> + Send { + async move { + let nonce_seed = thread_rng().gen::<[u8; 32]>(); + let ordered_public_keys = ctx.pubkeys().iter().cloned().map(|p| p.into()).collect(); + let first_round = FirstRound::new( + ctx, + nonce_seed, + signer_idx, + SecNonceSpices::new().with_seckey(self.key.clone()), + ) + .map_err(|e| SignerIdxOutOfBounds { + index: e.index, + n_signers: e.n_signers, + })?; + Ok(ServerFirstRound { + first_round, + ordered_public_keys, + seckey: self.key.clone(), + }) + } + } +} + +pub struct SledRoundPersist { + first_rounds: Tree, + second_rounds: Tree, +} + +impl SledRoundPersist { + pub fn new(first_rounds: Tree, second_rounds: Tree) -> Self { + Self { + first_rounds, + second_rounds, + } + } +} + +impl RoundPersister for SledRoundPersist { + type Error = OneOf<(rancor::Error, sled::Error)>; + + fn persist_first_round( + &self, + session_id: Musig2SessionId, + first_round: &ServerFirstRound, + ) -> impl Future> + Send { + async move { + let bytes = rkyv::to_bytes::(first_round).map_err(OneOf::new)?; + self.first_rounds + .insert(&session_id.to_be_bytes(), bytes.as_ref()) + .map_err(OneOf::new)?; + self.first_rounds.flush_async().await.map_err(OneOf::new)?; + Ok(()) + } + } + + fn persist_second_round( + &self, + session_id: Musig2SessionId, + second_round: &ServerSecondRound, + ) -> impl Future> + Send { + async move { + let bytes = rkyv::to_bytes::(second_round).map_err(OneOf::new)?; + self.second_rounds + .insert(&session_id.to_be_bytes(), bytes.as_ref()) + .map_err(OneOf::new)?; + self.second_rounds.flush_async().await.map_err(OneOf::new)?; + Ok(()) + } + } + + fn delete_first_round( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send { + async move { + self.first_rounds + .remove(&session_id.to_be_bytes()) + .map_err(OneOf::new)?; + self.first_rounds.flush_async().await.map_err(OneOf::new)?; + Ok(()) + } + } + + fn delete_second_round( + &self, + session_id: Musig2SessionId, + ) -> impl Future> + Send { + async move { + self.second_rounds + .remove(&session_id.to_be_bytes()) + .map_err(OneOf::new)?; + self.second_rounds.flush_async().await.map_err(OneOf::new)?; + Ok(()) + } + } + + fn load_first_rounds( + &self, + ) -> impl Future, Self::Error>> + Send + { + async move { + Ok(self + .first_rounds + .iter() + .map(|res| { + let (session_id_bytes, bytes) = res.map_err(OneOf::new)?; + let session_id = Musig2SessionId::from_be_bytes( + session_id_bytes + .as_ref() + .try_into() + .expect("valid session id"), + ); + let first_round = rkyv::from_bytes::(&bytes) + .map_err(OneOf::new)?; + Ok((session_id, first_round)) + }) + .collect::, Self::Error>>()?) + } + } + + fn load_second_rounds( + &self, + ) -> impl Future, Self::Error>> + Send + { + async move { + Ok(self + .second_rounds + .iter() + .map(|res| { + let (session_id_bytes, bytes) = res.map_err(OneOf::new)?; + let session_id = Musig2SessionId::from_be_bytes( + session_id_bytes + .as_ref() + .try_into() + .expect("valid session id"), + ); + let second_round = rkyv::from_bytes::(&bytes) + .map_err(OneOf::new)?; + Ok((session_id, second_round)) + }) + .collect::, Self::Error>>()?) + } + } +} + +#[derive(Archive, Serialize, Deserialize)] +pub struct ServerFirstRound { + first_round: FirstRound, + #[rkyv(with = Map)] + ordered_public_keys: Vec, + #[rkyv(with = musig2::rkyv_wrappers::SecretKey)] + seckey: SecretKey, +} + +impl Musig2SignerFirstRound for ServerFirstRound { + fn our_nonce( + &self, + ) -> impl Future::Container> + Send { + async move { self.first_round.our_public_nonce() } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send { + async move { + self.first_round + .holdouts() + .iter() + .map(|idx| self.ordered_public_keys[*idx]) + .collect() + } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { self.first_round.is_complete() } + } + + fn receive_pub_nonce( + &mut self, + pubkey: PublicKey, + pubnonce: musig2::PubNonce, + ) -> impl Future::Container>> + Send + { + async move { + let signer_idx = self + .ordered_public_keys + .iter() + .position(|x| x == &pubkey) + .ok_or(RoundContributionError::out_of_range(0, 0))?; + self.first_round.receive_nonce(signer_idx, pubnonce) + } + } + + fn finalize( + self, + hash: [u8; 32], + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { + self.first_round + .finalize(self.seckey, hash) + .map(|sr| ServerSecondRound { + second_round: sr, + ordered_public_keys: self.ordered_public_keys, + }) + } + } +} + +#[derive(Archive, Serialize, Deserialize)] +pub struct ServerSecondRound { + second_round: SecondRound<[u8; 32]>, + #[rkyv(with = Map)] + ordered_public_keys: Vec, +} + +impl Musig2SignerSecondRound for ServerSecondRound { + fn agg_nonce( + &self, + ) -> impl Future::Container> + Send { + async move { self.second_round.aggregated_nonce().clone() } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send { + async move { + self.second_round + .holdouts() + .into_iter() + .map(|idx| self.ordered_public_keys[*idx]) + .collect() + } + } + + fn our_signature( + &self, + ) -> impl Future::Container> + Send { + async move { self.second_round.our_signature() } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { self.second_round.is_complete() } + } + + fn receive_signature( + &mut self, + pubkey: PublicKey, + signature: musig2::PartialSignature, + ) -> impl Future::Container>> + Send + { + async move { + let signer_idx = self + .ordered_public_keys + .iter() + .position(|x| x == &pubkey) + .ok_or(RoundContributionError::out_of_range(0, 0))?; + self.second_round.receive_signature(signer_idx, signature) + } + } + + fn finalize( + self, + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { self.second_round.finalize() } + } +} diff --git a/crates/secret-service/src/disk/operator.rs b/crates/secret-service/src/disk/operator.rs index e69de29b..25c3a52c 100644 --- a/crates/secret-service/src/disk/operator.rs +++ b/crates/secret-service/src/disk/operator.rs @@ -0,0 +1,32 @@ +use std::future::Future; + +use bitcoin::key::Keypair; +use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; +use secret_service_proto::v1::traits::{OperatorSigner, Origin, Server}; + +pub struct Operator { + kp: Keypair, +} + +impl Operator { + pub fn new(sk: SecretKey) -> Self { + let kp = Keypair::from_secret_key(SECP256K1, &sk); + Self { kp } + } +} + +impl OperatorSigner for Operator { + fn sign( + &self, + digest: &[u8; 32], + ) -> impl Future::Container> + Send { + async move { + self.kp + .sign_schnorr(Message::from_digest_slice(digest).unwrap()) + } + } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { self.kp.public_key() } + } +} diff --git a/crates/secret-service/src/disk/p2p.rs b/crates/secret-service/src/disk/p2p.rs index e69de29b..3933ac96 100644 --- a/crates/secret-service/src/disk/p2p.rs +++ b/crates/secret-service/src/disk/p2p.rs @@ -0,0 +1,29 @@ +use std::future::Future; + +use bitcoin::key::Keypair; +use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; +use secret_service_proto::v1::traits::{P2PSigner, Server}; + +pub struct ServerP2PSigner { + kp: Keypair, +} + +impl ServerP2PSigner { + pub fn new(sk: SecretKey) -> Self { + let kp = Keypair::from_secret_key(SECP256K1, &sk); + Self { kp } + } +} + +impl P2PSigner for ServerP2PSigner { + fn sign(&self, digest: &[u8; 32]) -> impl Future + Send { + async move { + self.kp + .sign_schnorr(Message::from_digest_slice(digest).unwrap()) + } + } + + fn pubkey(&self) -> impl Future + Send { + async move { self.kp.public_key() } + } +} diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs index e69de29b..74f483b3 100644 --- a/crates/secret-service/src/disk/stakechain.rs +++ b/crates/secret-service/src/disk/stakechain.rs @@ -0,0 +1,27 @@ +use std::future::Future; + +use hkdf::Hkdf; +use secret_service_proto::v1::traits::{Server, StakeChainPreimages}; +use sha2::Sha256; + +pub struct StakeChain { + seed: [u8; 32], +} + +impl StakeChain { + pub fn new(seed: [u8; 32]) -> Self { + Self { seed } + } +} + +impl StakeChainPreimages for StakeChain { + fn get_preimg(&self, deposit_index: u64) -> impl Future + Send { + async move { + let hk = Hkdf::::new(Some(&deposit_index.to_le_bytes()), &self.seed); + let mut okm = [0u8; 32]; + hk.expand(b"strata-bridge-stakechain", &mut okm) + .expect("32 is a valid length for Sha256 to output"); + okm + } + } +} diff --git a/crates/secret-service/src/disk/wots.rs b/crates/secret-service/src/disk/wots.rs index e69de29b..1ed94632 100644 --- a/crates/secret-service/src/disk/wots.rs +++ b/crates/secret-service/src/disk/wots.rs @@ -0,0 +1,34 @@ +use std::future::Future; + +use bitcoin::{hashes::Hash, Txid}; +use hkdf::Hkdf; +use secret_service_proto::v1::traits::{Server, WotsSigner}; +use sha2::Sha256; + +pub struct SeededWotsSigner { + seed: [u8; 32], +} + +impl SeededWotsSigner { + pub fn new(seed: [u8; 32]) -> Self { + Self { seed } + } +} + +impl WotsSigner for SeededWotsSigner { + fn get_key(&self, index: u64, txid: Txid) -> impl Future + Send { + async move { + let salt = { + let mut buf = [0; 32 + size_of::()]; + buf[..32].copy_from_slice(txid.as_raw_hash().as_byte_array()); + buf[32..].copy_from_slice(&index.to_le_bytes()); + buf + }; + let hk = Hkdf::::new(Some(&salt), &self.seed); + let mut okm = [0u8; 64]; + hk.expand(b"strata-bridge-winternitz", &mut okm) + .expect("64 is a valid length for Sha256 to output"); + okm + } + } +} diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 7d8109fc..edf0abb5 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,40 +1,21 @@ // use secret_service_server::rustls::ServerConfig; +pub mod config; pub mod disk; -use std::{ - cell::RefCell, - env::args, - future::Future, - io, - net::SocketAddr, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{env::args, path::PathBuf, str::FromStr}; -use musig2::{ - errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::PublicKey, - FirstRound, KeyAggContext, LiftedSignature, SecondRound, -}; -use parking_lot::Mutex; -use rand::{thread_rng, Rng}; -use rkyv::rancor; -use secret_service_proto::v1::traits::{ - Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, - Origin, SecretService, Server, -}; +use config::{TlsConfig, TomlConfig}; +use disk::Service; use secret_service_server::{ run_server, rustls::{ pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, ServerConfig, }, - Config, RoundPersister, + Config, }; -use sled::{Db, Tree}; -use terrors::OneOf; -use tokio::{fs, task::spawn_blocking}; +use tokio::fs; use tracing::info; #[tokio::main] @@ -98,264 +79,9 @@ async fn main() { .await .expect("good service"); - run_server(config, service.into()).unwrap().await; -} - -#[derive(serde::Deserialize)] -struct TomlConfig { - tls: Option, - transport: TransportConfig, - seed: Option, - db: Option, -} - -#[derive(serde::Deserialize)] -struct TransportConfig { - addr: SocketAddr, - conn_limit: Option, -} - -#[derive(serde::Deserialize)] -struct TlsConfig { - cert: Option, - key: Option, -} - -struct Service { - seed: [u8; 32], - db: Db, -} - -impl Service { - async fn load_from_seed_and_db(seed_path: &Path, db_path: PathBuf) -> io::Result { - let mut seed = [0; 32]; - - if let Some(parent) = seed_path.parent() { - fs::create_dir_all(parent).await?; - } - - match fs::read(seed_path).await { - Ok(vec) => seed.copy_from_slice(&vec), - Err(e) if e.kind() == io::ErrorKind::NotFound => { - let mut rng = rand::thread_rng(); - rng.fill(&mut seed); - fs::write(seed_path, &seed).await?; - } - Err(e) => return Err(e), - }; - - let db = spawn_blocking(move || sled::open(db_path)) - .await - .expect("thread ok")?; - Ok(Self { seed, db }) - } -} - -struct ServerFirstRound { - session_id: Musig2SessionId, - tree: Tree, - first_round: FirstRound, - ordered_public_keys: Vec, -} - -impl RoundPersister for ServerFirstRound { - type Error = OneOf<(rancor::Error, sled::Error)>; - - fn persist( - &self, - session_id: Musig2SessionId, - ) -> impl Future> + Send { - async move { - let bytes = rkyv::to_bytes::(&self.first_round).map_err(OneOf::new)?; - self.tree - .insert(&session_id.to_be_bytes(), bytes.as_ref()) - .map_err(OneOf::new)?; - self.tree.flush_async().await.map_err(OneOf::new)?; - Ok(()) - } - } -} - -impl Musig2SignerFirstRound for ServerFirstRound { - fn our_nonce( - &self, - ) -> impl Future::Container> + Send { - async move { todo!() } - } - - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { todo!() } - } - - fn is_complete(&self) -> impl Future::Container> + Send { - async move { todo!() } - } - - fn receive_pub_nonce( - &self, - pubkey: PublicKey, - pubnonce: musig2::PubNonce, - ) -> impl Future::Container>> + Send - { - async move { todo!() } - } - - fn finalize( - self, - hash: [u8; 32], - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { todo!() } - } -} - -struct ServerSecondRound { - session_id: Musig2SessionId, - tree: Tree, - second_round: Mutex>, - ordered_public_keys: Mutex>, -} - -impl RoundPersister for ServerSecondRound { - type Error = OneOf<(rancor::Error, sled::Error)>; - - fn persist( - &self, - session_id: Musig2SessionId, - ) -> impl Future> + Send { - async move { - let bytes = - rkyv::to_bytes::(&*self.second_round.lock()).map_err(OneOf::new)?; - self.tree - .insert(&session_id.to_be_bytes(), bytes.as_ref()) - .map_err(OneOf::new)?; - self.tree.flush_async().await.map_err(OneOf::new)?; - Ok(()) - } - } -} - -impl Musig2SignerSecondRound for ServerSecondRound { - fn agg_nonce( - &self, - ) -> impl Future::Container> + Send { - async move { self.second_round.lock().aggregated_nonce().clone() } - } - - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - let ordered_public_keys = self.ordered_public_keys.lock(); - self.second_round - .lock() - .holdouts() - .into_iter() - .map(|idx| ordered_public_keys[*idx]) - .collect() - } - } - - fn our_signature( - &self, - ) -> impl Future::Container> + Send { - async move { self.second_round.lock().our_signature() } - } - - fn is_complete(&self) -> impl Future::Container> + Send { - async move { self.second_round.lock().is_complete() } - } - - fn receive_signature( - &self, - pubkey: PublicKey, - signature: musig2::PartialSignature, - ) -> impl Future::Container>> + Send - { - async move { - let signer_idx = self - .ordered_public_keys - .lock() - .iter() - .position(|x| x == &pubkey) - .ok_or(RoundContributionError::out_of_range(0, 0))?; - self.second_round - .lock() - .receive_signature(signer_idx, signature) - } - } - - fn finalize( - self, - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { self.second_round.into_inner().finalize() } - } -} - -struct Operator; - -impl OperatorSigner for Operator { - fn sign_psbt(&self, psbt: bitcoin::Psbt) -> impl Future + Send { - async move { todo!() } - } -} - -struct P2PSigner; - -impl SecretService for Service { - type OperatorSigner = Operator; - - type P2PSigner; - - type Musig2Signer; - - type WotsSigner; - - fn operator_signer(&self) -> Self::OperatorSigner { - todo!() - } - - fn p2p_signer(&self) -> Self::P2PSigner { - todo!() - } - - fn musig2_signer(&self) -> Self::Musig2Signer { - todo!() - } - - fn wots_signer(&self) -> Self::WotsSigner { - todo!() - } -} - -struct Ms2Signer { - tree: Tree, -} + let persister = service.round_persister().expect("good persister"); -impl Musig2Signer for Ms2Signer { - fn new_session( - &self, - public_keys: Vec, - ) -> impl Future + Send { - async move { - let nonce_seed = thread_rng().gen::<[u8; 32]>(); - let first_round = FirstRound::new( - KeyAggContext::new(public_keys), - nonce_seed, - signer_index, - spices, - ); - ServerFirstRound { - session_id, - tree: self.tree.clone(), - first_round, - ordered_public_keys: public_keys, - } - } - } + run_server(config, service.into(), persister.into()) + .unwrap() + .await; } From 40a122e928975769534040caa9a733568ef56f6e Mon Sep 17 00:00:00 2001 From: Azz Date: Sun, 16 Feb 2025 18:24:20 +0000 Subject: [PATCH 10/30] TaprootWitness for musig2 signer --- crates/secret-service-client/Cargo.toml | 1 + crates/secret-service-client/src/lib.rs | 22 +++++- crates/secret-service-proto/Cargo.toml | 1 + crates/secret-service-proto/src/v1/traits.rs | 6 +- crates/secret-service-proto/src/v1/wire.rs | 73 +++++++++++++++++++- crates/secret-service-server/Cargo.toml | 1 + crates/secret-service-server/src/lib.rs | 27 ++++++-- crates/secret-service/Cargo.toml | 1 + crates/secret-service/src/disk/musig2.rs | 51 +++++++++++--- 9 files changed, 160 insertions(+), 23 deletions(-) diff --git a/crates/secret-service-client/Cargo.toml b/crates/secret-service-client/Cargo.toml index e01ceeb2..87b27a38 100644 --- a/crates/secret-service-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -10,6 +10,7 @@ musig2 = { path = "../musig2" } quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } +strata-bridge-primitives.workspace = true terrors.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 33d0f3d7..8df7802c 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -31,6 +31,7 @@ use secret_service_proto::{ WireMessage, }, }; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; use tokio::time::timeout; @@ -428,12 +429,15 @@ struct Musig2Client { impl Musig2Signer for Musig2Client { fn new_session( &self, - ctx: KeyAggContext, - signer_idx: usize, + pubkeys: Vec, + witness: TaprootWitness, ) -> impl Future, ClientError>> + Send { async move { - let msg = ClientMessage::Musig2NewSession { ctx, signer_idx }; + let msg = ClientMessage::Musig2NewSession { + pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), + witness: witness.into(), + }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2NewSession(maybe_session_id) = res else { return Err(ClientError::ProtocolError(res)); @@ -449,6 +453,18 @@ impl Musig2Signer for Musig2Client { }) } } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2Pubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2Pubkey { pubkey } = res else { + return Err(ClientError::ProtocolError(res)); + }; + + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::ProtocolError(res)) + } + } } struct WotsClient { diff --git a/crates/secret-service-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml index 54ec29ba..a5168bca 100644 --- a/crates/secret-service-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -8,3 +8,4 @@ bitcoin.workspace = true musig2 = { path = "../musig2" } quinn.workspace = true rkyv.workspace = true +strata-bridge-primitives.workspace = true diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 9844dd03..9be9655f 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -8,6 +8,7 @@ use musig2::{ }; use quinn::{ConnectionError, ReadExactError, WriteError}; use rkyv::{rancor, Archive, Deserialize, Serialize}; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::wire::ServerMessage; @@ -63,9 +64,10 @@ pub struct SignerIdxOutOfBounds { pub trait Musig2Signer: Send + Sync { fn new_session( &self, - ctx: KeyAggContext, - signer_idx: usize, + pubkeys: Vec, + witness: TaprootWitness, ) -> impl Future>> + Send; + fn pubkey(&self) -> impl Future> + Send; } pub trait Musig2SignerFirstRound: Send + Sync { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index f5cec60c..3f09261a 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -1,8 +1,14 @@ +use bitcoin::{ + hashes::Hash, + taproot::{ControlBlock, TaprootError}, + ScriptBuf, TapNodeHash, +}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, KeyAggContext, }; use rkyv::{with::Map, Archive, Deserialize, Serialize}; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::traits::{Musig2SessionId, SignerIdxOutOfBounds}; @@ -26,6 +32,9 @@ pub enum ServerMessage { }, Musig2NewSession(Result), + Musig2Pubkey { + pubkey: [u8; 33], + }, Musig2FirstRoundOurNonce { our_nonce: [u8; 66], @@ -110,9 +119,10 @@ pub enum ClientMessage { P2PPubkey, Musig2NewSession { - ctx: KeyAggContext, - signer_idx: usize, + pubkeys: Vec<[u8; 33]>, + witness: SerializableTaprootWitness, }, + Musig2Pubkey, Musig2FirstRoundOurNonce { session_id: usize, @@ -163,3 +173,62 @@ pub enum ClientMessage { deposit_idx: u64, }, } + +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +pub enum SerializableTaprootWitness { + Key, + Script { + script_buf: Vec, + control_block: Vec, + }, + Tweaked { + tweak: [u8; 32], + }, +} + +impl From for SerializableTaprootWitness { + fn from(witness: TaprootWitness) -> Self { + match witness { + TaprootWitness::Key => SerializableTaprootWitness::Key, + TaprootWitness::Script { + script_buf, + control_block, + } => SerializableTaprootWitness::Script { + script_buf: script_buf.into_bytes(), + control_block: control_block.serialize(), + }, + TaprootWitness::Tweaked { tweak } => SerializableTaprootWitness::Tweaked { + tweak: tweak.to_raw_hash().to_byte_array(), + }, + } + } +} + +pub enum TaprootWitnessError { + InvalidWitnessType, + InvalidScriptControlBlock(TaprootError), +} + +impl TryFrom for TaprootWitness { + type Error = TaprootWitnessError; + fn try_from(value: SerializableTaprootWitness) -> Result { + match value { + SerializableTaprootWitness::Key => Ok(TaprootWitness::Key), + SerializableTaprootWitness::Script { + script_buf, + control_block, + } => { + let script_buf = ScriptBuf::from_bytes(script_buf); + let control_block = ControlBlock::decode(&control_block) + .map_err(TaprootWitnessError::InvalidScriptControlBlock)?; + Ok(TaprootWitness::Script { + script_buf, + control_block, + }) + } + SerializableTaprootWitness::Tweaked { tweak } => Ok(TaprootWitness::Tweaked { + tweak: TapNodeHash::from_byte_array(tweak), + }), + } + } +} diff --git a/crates/secret-service-server/Cargo.toml b/crates/secret-service-server/Cargo.toml index dece1dc5..4dde12d0 100644 --- a/crates/secret-service-server/Cargo.toml +++ b/crates/secret-service-server/Cargo.toml @@ -12,6 +12,7 @@ parking_lot.workspace = true quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } +strata-bridge-primitives.workspace = true terrors.workspace = true tokio.workspace = true tracing.workspace = true diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 0c45f7cb..cfc2d1db 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -34,6 +34,7 @@ use secret_service_proto::{ }, wire::{ArchivedVersionedClientMessage, LengthUint, VersionedServerMessage, WireMessage}, }; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; use tokio::{ sync::Mutex, @@ -238,15 +239,25 @@ where } } - ArchivedClientMessage::Musig2NewSession { ctx, signer_idx } => 'block: { + ArchivedClientMessage::Musig2NewSession { pubkeys, witness } => 'block: { let signer = service.musig2_signer(); - let Ok(ctx) = deserialize::(ctx) else { + let Ok(ser_witness) = deserialize::<_, rancor::Error>(witness) else { break 'block ServerMessage::InvalidClientMessage; }; - let first_round = match signer - .new_session(ctx, signer_idx.to_native() as usize) - .await - { + let Ok(witness) = TaprootWitness::try_from(ser_witness) + .map_err(|_| ServerMessage::InvalidClientMessage) + else { + break 'block ServerMessage::InvalidClientMessage; + }; + let Ok(pubkeys) = pubkeys + .into_iter() + .map(|data| PublicKey::from_slice(data)) + .collect::, _>>() + else { + break 'block ServerMessage::InvalidClientMessage; + }; + + let first_round = match signer.new_session(pubkeys, witness).await { Ok(fr) => fr, Err(e) => break 'block ServerMessage::Musig2NewSession(Err(e)), }; @@ -266,6 +277,10 @@ where ServerMessage::Musig2NewSession(Ok(write_perm.session_id())) } + ArchivedClientMessage::Musig2Pubkey => ServerMessage::Musig2Pubkey { + pubkey: service.musig2_signer().pubkey().await.serialize(), + }, + ArchivedClientMessage::Musig2FirstRoundOurNonce { session_id } => { let r = musig2_sm .lock() diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index abb34aaf..3f0a06bd 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -18,6 +18,7 @@ secret-service-server = { version = "0.1.0", path = "../secret-service-server" } serde.workspace = true sha2.workspace = true sled = "0.34.7" +strata-bridge-primitives.workspace = true strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } terrors.workspace = true diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index 18eccb8a..6d866acd 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -1,8 +1,9 @@ use std::future::Future; +use bitcoin::key::Keypair; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::{PublicKey, SecretKey}, + secp256k1::{PublicKey, SecretKey, SECP256K1}, FirstRound, KeyAggContext, LiftedSignature, SecNonceSpices, SecondRound, }; use rand::{thread_rng, Rng}; @@ -13,32 +14,58 @@ use secret_service_proto::v1::traits::{ }; use secret_service_server::RoundPersister; use sled::Tree; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; pub struct Ms2Signer { - key: SecretKey, + kp: Keypair, } impl Ms2Signer { pub fn new(key: SecretKey) -> Self { - Self { key } + Self { + kp: Keypair::from_secret_key(SECP256K1, &key), + } } } impl Musig2Signer for Ms2Signer { fn new_session( &self, - ctx: KeyAggContext, - signer_idx: usize, + mut pubkeys: Vec, + witness: TaprootWitness, ) -> impl Future> + Send { async move { let nonce_seed = thread_rng().gen::<[u8; 32]>(); - let ordered_public_keys = ctx.pubkeys().iter().cloned().map(|p| p.into()).collect(); + if !pubkeys.contains(&self.kp.public_key()) { + pubkeys.push(self.kp.public_key()); + } + pubkeys.sort(); + let signer_index = pubkeys + .iter() + .position(|pk| pk == &self.kp.public_key()) + .unwrap(); + let mut ctx = KeyAggContext::new(pubkeys.clone()).unwrap(); + + match witness { + TaprootWitness::Key => { + ctx = ctx + .with_unspendable_taproot_tweak() + .expect("must be able to tweak the key agg context") + } + TaprootWitness::Tweaked { tweak } => { + ctx = ctx + .with_taproot_tweak(tweak.as_ref()) + .expect("must be able to tweak the key agg context") + } + _ => {} + } + let first_round = FirstRound::new( ctx, nonce_seed, - signer_idx, - SecNonceSpices::new().with_seckey(self.key.clone()), + signer_index, + SecNonceSpices::new().with_seckey(self.kp.secret_key()), ) .map_err(|e| SignerIdxOutOfBounds { index: e.index, @@ -46,11 +73,15 @@ impl Musig2Signer for Ms2Signer { })?; Ok(ServerFirstRound { first_round, - ordered_public_keys, - seckey: self.key.clone(), + ordered_public_keys: pubkeys, + seckey: self.kp.secret_key(), }) } } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { self.kp.public_key() } + } } pub struct SledRoundPersist { From 928cadc4e42822fc341dce3c8f2d7d11e4b643e5 Mon Sep 17 00:00:00 2001 From: Azz Date: Sun, 16 Feb 2025 18:39:56 +0000 Subject: [PATCH 11/30] remove persistence + musig2 fork --- Cargo.toml | 1 - crates/musig2/.gitignore | 3 - crates/musig2/Cargo.toml | 52 - crates/musig2/LICENSE | 24 - crates/musig2/Makefile | 44 - crates/musig2/README.md | 49 - crates/musig2/doc/API.md | 742 ------------- crates/musig2/doc/adaptor_signatures.md | 246 ----- crates/musig2/reference.py | 882 ---------------- crates/musig2/src/binary_encoding.rs | 295 ------ crates/musig2/src/bip340.rs | 445 -------- crates/musig2/src/deterministic.rs | 147 --- crates/musig2/src/errors.rs | 386 ------- crates/musig2/src/key_agg.rs | 988 ----------------- crates/musig2/src/key_sort.rs | 22 - crates/musig2/src/lib.rs | 55 - crates/musig2/src/nonces.rs | 992 ------------------ crates/musig2/src/rkyv_wrappers.rs | 190 ---- crates/musig2/src/rounds.rs | 724 ------------- crates/musig2/src/sig_agg.rs | 283 ----- crates/musig2/src/signature.rs | 432 -------- crates/musig2/src/signing.rs | 615 ----------- crates/musig2/src/tagged_hashes.rs | 208 ---- .../src/test_vectors/bip340_vectors.csv | 20 - .../src/test_vectors/key_agg_vectors.json | 88 -- .../src/test_vectors/key_sort_vectors.json | 18 - .../src/test_vectors/nonce_agg_vectors.json | 51 - .../src/test_vectors/nonce_gen_vectors.json | 34 - .../src/test_vectors/sig_agg_vectors.json | 151 --- .../src/test_vectors/sign_verify_vectors.json | 212 ---- .../src/test_vectors/tweak_vectors.json | 84 -- crates/musig2/src/testhex.rs | 93 -- .../tests/fuzz_against_reference_impl.rs | 317 ------ crates/secret-service-client/Cargo.toml | 2 +- crates/secret-service-client/src/lib.rs | 2 +- crates/secret-service-proto/Cargo.toml | 2 +- crates/secret-service-proto/src/v1/traits.rs | 2 +- crates/secret-service-proto/src/v1/wire.rs | 5 +- crates/secret-service-server/Cargo.toml | 2 +- crates/secret-service-server/src/lib.rs | 113 +- crates/secret-service-server/src/ms2sm.rs | 2 +- crates/secret-service/Cargo.toml | 2 +- crates/secret-service/src/disk/mod.rs | 23 +- crates/secret-service/src/disk/musig2.rs | 133 +-- crates/secret-service/src/main.rs | 10 +- 45 files changed, 28 insertions(+), 9163 deletions(-) delete mode 100644 crates/musig2/.gitignore delete mode 100644 crates/musig2/Cargo.toml delete mode 100644 crates/musig2/LICENSE delete mode 100644 crates/musig2/Makefile delete mode 100644 crates/musig2/README.md delete mode 100644 crates/musig2/doc/API.md delete mode 100644 crates/musig2/doc/adaptor_signatures.md delete mode 100644 crates/musig2/reference.py delete mode 100644 crates/musig2/src/binary_encoding.rs delete mode 100644 crates/musig2/src/bip340.rs delete mode 100644 crates/musig2/src/deterministic.rs delete mode 100644 crates/musig2/src/errors.rs delete mode 100644 crates/musig2/src/key_agg.rs delete mode 100644 crates/musig2/src/key_sort.rs delete mode 100644 crates/musig2/src/lib.rs delete mode 100644 crates/musig2/src/nonces.rs delete mode 100644 crates/musig2/src/rkyv_wrappers.rs delete mode 100644 crates/musig2/src/rounds.rs delete mode 100644 crates/musig2/src/sig_agg.rs delete mode 100644 crates/musig2/src/signature.rs delete mode 100644 crates/musig2/src/signing.rs delete mode 100644 crates/musig2/src/tagged_hashes.rs delete mode 100644 crates/musig2/src/test_vectors/bip340_vectors.csv delete mode 100644 crates/musig2/src/test_vectors/key_agg_vectors.json delete mode 100644 crates/musig2/src/test_vectors/key_sort_vectors.json delete mode 100644 crates/musig2/src/test_vectors/nonce_agg_vectors.json delete mode 100644 crates/musig2/src/test_vectors/nonce_gen_vectors.json delete mode 100644 crates/musig2/src/test_vectors/sig_agg_vectors.json delete mode 100644 crates/musig2/src/test_vectors/sign_verify_vectors.json delete mode 100644 crates/musig2/src/test_vectors/tweak_vectors.json delete mode 100644 crates/musig2/src/testhex.rs delete mode 100644 crates/musig2/tests/fuzz_against_reference_impl.rs diff --git a/Cargo.toml b/Cargo.toml index 989b2a74..108a875c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ members = [ "crates/secret-service-proto", "crates/secret-service-client", "crates/secret-service-server", - "crates/musig2", # binaries listed separately "bin/strata-bridge", diff --git a/crates/musig2/.gitignore b/crates/musig2/.gitignore deleted file mode 100644 index 6430f5e0..00000000 --- a/crates/musig2/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -__pycache__ -/Cargo.lock diff --git a/crates/musig2/Cargo.toml b/crates/musig2/Cargo.toml deleted file mode 100644 index dd612494..00000000 --- a/crates/musig2/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "musig2" -version = "0.1.0" -edition = "2021" -authors = ["conduition "] -description = "Flexible Rust implementation of the MuSig2 multisignature protocol, compatible with Bitcoin." -readme = "README.md" -license = "Unlicense" -repository = "https://github.com/conduition/musig2" -keywords = ["musig", "schnorr", "bitcoin", "multisignature", "musig2"] -include = ["/src", "!/src/test_vectors", "*.md"] - -[dependencies] -base16ct = { version = "0.2.0", default-features = false, features = ["alloc"] } -hmac = { version = "0.12.1", default-features = false, features = [] } -k256 = { version = "0.13.1", default-features = false, optional = true } -once_cell = { version = "1.18.0", default-features = false } -rand = { version = "0.8.5", optional = true, default-features = false, features = [ - "std_rng", -] } -rkyv.workspace = true -secp = { version = "0.3", default-features = false } -secp256k1 = { version = "0.29", optional = true, default-features = false } -serde = { version = "1.0.188", default-features = false, optional = true } -serdect = { version = "0.2.0", default-features = false, optional = true, features = [ - "alloc", -] } -sha2 = { version = "0.10.8", default-features = false } -subtle = { version = "2.5.0", default-features = false } - -[dev-dependencies] -serde = { version = "1.0.188", features = ["serde_derive"] } -serde_json = "1.0.107" -csv = "1.3.0" -serdect = "0.2.0" -rand = "0.8.5" -secp = { version = "0.3", default-features = false, features = [ - "serde", - "rand", - "secp256k1-invert", -] } - -[features] -default = ["secp256k1"] -secp256k1 = ["dep:secp256k1", "secp/secp256k1"] -# k256 = ["dep:k256", "secp/k256"] -serde = ["dep:serde", "secp/serde", "dep:serdect"] -rand = ["dep:rand", "secp/rand"] - -[package.metadata.docs.rs] -all-features = true -rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/musig2/LICENSE b/crates/musig2/LICENSE deleted file mode 100644 index fdddb29a..00000000 --- a/crates/musig2/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -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 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. - -For more information, please refer to diff --git a/crates/musig2/Makefile b/crates/musig2/Makefile deleted file mode 100644 index e38deb74..00000000 --- a/crates/musig2/Makefile +++ /dev/null @@ -1,44 +0,0 @@ -.PHONY: check check-* test test-* -check: check-default check-mixed check-secp256k1 check-k256 - -# Checks the source code with default features enabled. -check-default: - cargo clippy - -# Checks the source code with all features enabled. -check-mixed: - cargo clippy --all-features - cargo clippy --all-features --tests - -# Checks the source code with variations of libsecp256k1 feature sets. -check-secp256k1: - cargo clippy --no-default-features --features secp256k1 - cargo clippy --no-default-features --features secp256k1,serde - cargo clippy --no-default-features --features secp256k1,serde,rand - cargo clippy --no-default-features --features secp256k1,serde,rand --tests - -# Checks the source code with variations of pure-rust feature sets. -check-k256: - cargo clippy --no-default-features --features k256 - cargo clippy --no-default-features --features k256,serde - cargo clippy --no-default-features --features k256,serde,rand - cargo clippy --no-default-features --features k256,serde,rand --tests - - -test: test-default test-mixed test-secp256k1 test-k256 - -test-default: - cargo test - -test-mixed: - cargo test --all-features - -test-secp256k1: - cargo test --no-default-features --features secp256k1,serde,rand - -test-k256: - cargo test --no-default-features --features k256,serde,rand - -.PHONY: docwatch -docwatch: - watch -n 5 cargo doc --all-features diff --git a/crates/musig2/README.md b/crates/musig2/README.md deleted file mode 100644 index 5a71ed0f..00000000 --- a/crates/musig2/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# MuSig2 - -This crate provides a flexible rust implementation of [MuSig2](https://eprint.iacr.org/2020/1261), an optimized digital signature aggregation protocol, on the `secp256k1` elliptic curve. - -MuSig2 allows groups of mutually distrusting parties to cooperatively sign data and aggregate their signatures into a single aggregated signature which is indistinguishable from a signature made by a single private key. The group collectively controls an _aggregated public key_ which can only create signatures if everyone in the group cooperates (AKA an N-of-N multisignature scheme). MuSig2 is optimized to support secure signature aggregation with only **two round-trips of network communication.** - -Specifically, this crate implements [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki), for creating and verifying signatures which validate under Bitcoin consensus rules, but the protocol is flexible and can be applied to any N-of-N multisignature use-case. - -## ⚠️ Beta Status ⚠️ - -This crate is in beta status. The latest release is a `v0.0.x` version number. Expect breaking changes and security fixes. Once this crate is stabilized, we will tag and release `v1.0.0`. - -## Overview - -If you're not already familiar with MuSig2, the process of cooperative signing runs like so: - -1. All signers share their public keys with one-another. The group computes an _aggregated public key_ which they collectively control. -2. In the **first signing round,** signers generate and share _nonces_ (random numbers) with one-another. These nonces have both secret and public versions. Only the public nonce (AKA `PubNonce`) should be shared, while the corresponding secret nonce (AKA `SecNonce`) must be kept secret. -3. Once every signer has received the public nonces of every other signer, each signer makes a _partial signature_ for a message using their secret key and secret nonce. -4. In the **second signing round,** signers share their partial signatures with one-another. Partial signatures can be verified to place blame on misbehaving signers. -5. A valid set of partial signatures can be aggregated into a final signature, which is just a normal [Schnorr signature](https://en.wikipedia.org/wiki/Schnorr_signature), valid under the aggregated public key. - -## Choice of Backbone - -This crate does not implement elliptic curve point math directly. Instead we depend on one of two reputable libraries: - -- C bindings to [`libsecp256k1`](https://github.com/bitcoin-core/secp256k1), via [the `secp256k1` crate](https://crates.io/crates/secp256k1), maintained by the Bitcoin Core team. -- A pure-rust implementation via [the `k256` crate](https://crates.io/crates/k256), maintained by the [RustCrypto](https://github.com/RustCrypto) team. - -One or the other can be used. By default, this crate prefers to rely on `libsecp256k1`, as this is the most vetted and publicly trusted implementation of secp256k1 curve math available anywhere. However, if you need a pure-rust implementation, you can install this crate without it, and use the pure-rust `k256` crate instead. - -```notrust -cargo add musig2 --no-default-features --features k256 -``` - -If both `k256` and `secp256k1` features are enabled, then we default to using `libsecp256k1` bindings for the actual math, but still provide trait implementations to make this crate interoperable with `k256`. - -This crate internally represents elliptic curve points (e.g. public keys) and scalars (e.g. private keys) using the [`secp` crate](https://crates.io/crates/secp) and its types: - -- [`secp::Scalar`](https://docs.rs/secp/struct.Scalar.html) for non-zero scalar values. -- [`secp::Point`](https://docs.rs/secp/struct.Point.html) for non-infinity curve points -- [`secp::MaybeScalar`](https://docs.rs/secp/enum.Point.html) for possibly-zero scalars. -- [`secp::MaybePoint`](https://docs.rs/secp/enum.Point.html) for possibly-infinity curve points. - -Depending on which features of this crate are enabled, conversion traits are implemented between these types and higher-level types such as [`secp256k1::PublicKey`](https://docs.rs/secp256k1/struct.PublicKey.html) or [`k256::SecretKey`](https://docs.rs/k256/type.SecretKey.html). Generally, our API can accept or return any type that converts to/from the equivalent `secp` representations, although callers are also welcome to use `secp` directly too. - -## Documentation - -[Head on over to docs.rs to see the full API documentation and usage examples.](https://docs.rs/musig2) diff --git a/crates/musig2/doc/API.md b/crates/musig2/doc/API.md deleted file mode 100644 index 36f22179..00000000 --- a/crates/musig2/doc/API.md +++ /dev/null @@ -1,742 +0,0 @@ -# Features - -| Feature | Description | Dependencies | Enabled by Default | -|---------|-------------|--------------|:------------------:| -| `secp256k1` | Use [`libsecp256k1`](https://github.com/bitcoin-core/secp256k1) bindings for elliptic curve math. Include trait implementations for converting to and from types in [the `secp256k1` crate][secp256k1]. This feature supercedes the `k256` feature if that one is enabled. | [`secp256k1`] | ✅ | -| `k256` | Use [the `k256` crate][k256] for elliptic curve math. This allows a pure-rust implementation of MuSig2. Include trait implementations for types from [`k256`]. If the `secp256k1` feature is enabled, then [`k256`] will still be brought in and trait implementations will be included, but the actual curve math will be done by `libsecp256k1`. | [`k256`] | ❌ | -| `serde` | Implement serialization and deserialization for types in this crate. | [`serde`](https://docs.rs/serde) | ❌ | -| `rand` | Enable support for accepting a CSPRNG as input, via [the `rand` crate][rand] | [`rand`] | ❌ | - -# Key Aggregation - -Once all signers know each other's public keys (out of scope for this crate), they can construct a [`KeyAggContext`] which aggregates their public keys together, along with optional _tweak values_ (see [`KeyAggContext::with_tweak`] to learn more). - - -

-

Example

- -```rust -# #[cfg(feature = "secp256k1")] -use secp256k1::{SecretKey, PublicKey}; -# -# // k256::SecretKey and k256::PublicKey don't have string parsing traits, -# // so I'll just use our own representations for this example. -# #[cfg(not(feature = "secp256k1"))] -# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; -use musig2::KeyAggContext; - -let pubkeys = [ - "026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f4" - .parse::() - .unwrap(), - "02f3b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b" - .parse::() - .unwrap(), - "03204ea8bc3425b2cbc9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" - .parse::() - .unwrap(), -]; - -let signer_index = 2; -let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" - .parse() - .unwrap(); - -let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); - - -// This is the key which the group has control over. -let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); -assert_eq!( - aggregated_pubkey, - "02e272de44ea720667aba55341a1a761c0fc8fbe294aa31dbaf1cff80f1c2fd940" - .parse() - .unwrap() -); -``` -
-
- -A handy property of the MuSig2 protocol is that signers do not need proof that the other signers in the group know their own secret keys. They can simply exchange public keys and continue once all signers agree on an aggregated pubkey. - -Once you have a [`KeyAggContext`], you may choose between two sets of APIs for running the MuSig2 protocol, covering both **Functional** and **State-Machine** approaches. - -## State-Machine API - -A state machine is a stateful object which manipulates its internal state based on external input, fed to it by the caller (you). - -This crate's _State-Machine_-based signing API is safer, but may not be as flexible as the _Functional_ API. It is constructed around two stateful types, [`FirstRound`] and [`SecondRound`], which handle storing partial nonces and partial signatures. - -[`FirstRound`] is analagous to the first signing round of MuSig2, wherein signers generate and send nonces to one-another, or to a [designated aggregator](#single-aggregator). - -[`SecondRound`] is analagous to the second signing round where signers share and verify their partial signatures. Once the [`SecondRound`] complete, it can be finalized into a valid aggregated Schnorr signature. - - -
-

Example

- -```rust -# #[cfg(feature = "secp256k1")] -# use secp256k1::{SecretKey, PublicKey}; -# #[cfg(not(feature = "secp256k1"))] -# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; -# use musig2::KeyAggContext; -# -# /// Same pubkeys as in previous example -# let key_agg_ctx = -# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ -# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ -# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" -# .parse::() -# .unwrap(); -# -# let signer_index = 2; -# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" -# .parse() -# .unwrap(); -# -# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); -# -use musig2::{ - CompactSignature, FirstRound, PartialSignature, PubNonce, SecNonceSpices, SecondRound, -}; - -// The group wants to sign something! -let message = "hello interwebz!"; - -// Normally this should be sampled securely from a CSPRNG. -// let mut nonce_seed = [0u8; 32] -// rand::rngs::OsRng.fill_bytes(&mut nonce_seed); -let nonce_seed = [0xACu8; 32]; - -let mut first_round = FirstRound::new( - key_agg_ctx, - nonce_seed, - signer_index, - SecNonceSpices::new() - .with_seckey(seckey) - .with_message(&message), -) -.unwrap(); - -// We would share our public nonce with our peers. -assert_eq!( - first_round.our_public_nonce(), - "02d1e90616ea78a612dddfe97de7b5e7e1ceef6e64b7bc23b922eae30fa2475cca\ - 02e676a3af322965d53cc128597897ef4f84a8d8080b456e27836db70e5343a2bb" - .parse() - .unwrap(), - "Our public nonce should match" -); - -// We can see a list of which signers (by index) have yet to provide us -// with a nonce. -assert_eq!(first_round.holdouts(), &[0, 1]); - -// We receive the public nonces from our peers one at a time. -first_round.receive_nonce( - 0, - "02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ - 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45" - .parse::() - .unwrap() -) -.unwrap(); - -// `is_complete` provides a quick check to see whether we have nonces from -// every signer yet. -assert!(!first_round.is_complete()); - -// ...once we receive all their nonces... -first_round.receive_nonce( - 1, - "020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ - 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d" - .parse::() - .unwrap() -) -.unwrap(); - -// ... the round will be complete. -assert!(first_round.is_complete()); - -let mut second_round: SecondRound<&str> = first_round.finalize(seckey, message).unwrap(); - -// We could now send our partial signature to our peers. -// Be careful not to send your signature first if your peers -// might run away without surrendering their signatures in exchange! -let our_partial_signature: PartialSignature = second_round.our_signature(); -assert_eq!( - our_partial_signature, - "efd62850b959a76a462f1e42eb3cecc77a5a0982742fff2901456b7d1453a817" - .parse() - .unwrap() -); - -second_round.receive_signature( - 0, - "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" - .parse::() - .unwrap() -) -.expect("signer 0's partial signature should be valid"); - -// Same methods as on FirstRound are available for SecondRound. -assert!(!second_round.is_complete()); -assert_eq!(second_round.holdouts(), &[1]); - -// Receive a partial signature from one of our cosigners. This -// automatically verifies the partial signature and returns an -// error if the signature is invalid. -second_round.receive_signature( - 1, - "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" - .parse::() - .unwrap() -) -.expect("signer 1's partial signature should be valid"); - -assert!(second_round.is_complete()); - -// If all signatures were received successfully, finalizing the second round -// should succeed with overwhelming probability. -let final_signature: CompactSignature = second_round.finalize().unwrap(); - -assert_eq!( - final_signature.to_string(), - "38fbd82d1d27bb3401042062acfd4e7f54ce93ddf26a4ae87cf71568c1d4e8bb\ - 8fca20bb6f7bce2c5b54576d315b21eae31a614641afd227cda221fd6b1c54ea" -); - -musig2::verify_single( - aggregated_pubkey, - final_signature, - message -) -.expect("aggregated signature must be valid"); -``` -
-
- -## Functional API - -The _Functional_ API exposes the MuSig2 protocol through pure functions which accept read-only inputs and produce deterministic outputs. This obviously lacks internal state and it is thus entirely dependent on the caller to securely handle nonce state management. The caller is free to implement nonce state management however they like with this API. [Please read the warning below about nonce-reuse BEFORE attempting to use the Functional API](#nonce-reuse). - -Instead of using [`FirstRound`] and [`SecondRound`], the Functional API is exposed through these pure functions: - -- [`SecNonce::generate`] - Generate a secret nonce. -- [`AggNonce::sum`] - Aggregate public nonces together. -- [`sign_partial`] - Create a partial signature on a message. -- [`verify_partial`] - Verify a partial signature. -- [`aggregate_partial_signatures`] - Aggregate a collection of partial signatures into a final valid signature. - -
-

Example

- -```rust -# #[cfg(feature = "secp256k1")] -# use secp256k1::{SecretKey, PublicKey}; -# #[cfg(not(feature = "secp256k1"))] -# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; -# use musig2::{KeyAggContext, PartialSignature, PubNonce}; -# -# let signer_index = 2; -# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" -# .parse() -# .unwrap(); -# -# /// Same pubkeys as in previous example -# let key_agg_ctx = -# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ -# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ -# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" -# .parse::() -# .unwrap(); -# -# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); -# let message = "hello interwebz!"; -# let nonce_seed = [0xACu8; 32]; -use musig2::{AggNonce, SecNonce}; - -// This is how `FirstRound` derives the nonce internally. -let secnonce = SecNonce::build(nonce_seed) - .with_seckey(seckey) - .with_message(&message) - .with_aggregated_pubkey(aggregated_pubkey) - .with_extra_input(&(signer_index as u32).to_be_bytes()) - .build(); - -let our_public_nonce = secnonce.public_nonce(); -assert_eq!( - our_public_nonce, - "02d1e90616ea78a612dddfe97de7b5e7e1ceef6e64b7bc23b922eae30fa2475cca\ - 02e676a3af322965d53cc128597897ef4f84a8d8080b456e27836db70e5343a2bb" - .parse() - .unwrap() -); - -// ...Exchange nonces with peers... - -let public_nonces = [ - "02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ - 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45" - .parse::() - .unwrap(), - - "020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ - 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d" - .parse::() - .unwrap(), - - our_public_nonce, -]; - -// We manually aggregate the nonces together and then construct our partial signature. -let aggregated_nonce: AggNonce = public_nonces.iter().sum(); -let our_partial_signature: PartialSignature = musig2::sign_partial( - &key_agg_ctx, - seckey, - secnonce, - &aggregated_nonce, - message -) -.expect("error creating partial signature"); - -let partial_signatures = [ - "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" - .parse::() - .unwrap(), - "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" - .parse::() - .unwrap(), - our_partial_signature, -]; - -/// Signatures should be verified upon receipt and invalid signatures -/// should be blamed on the signer who sent them. -for (i, partial_signature) in partial_signatures.into_iter().enumerate() { - if i == signer_index { - // Don't bother verifying our own signature - continue; - } - - let their_pubkey: PublicKey = key_agg_ctx.get_pubkey(i).unwrap(); - let their_pubnonce = &public_nonces[i]; - - musig2::verify_partial( - &key_agg_ctx, - partial_signature, - &aggregated_nonce, - their_pubkey, - their_pubnonce, - message - ) - .expect("received invalid signature from a peer"); -} - -let final_signature: [u8; 64] = musig2::aggregate_partial_signatures( - &key_agg_ctx, - &aggregated_nonce, - partial_signatures, - message, -) -.expect("error aggregating signatures"); - -assert_eq!( - final_signature, - [ - 0x38, 0xFB, 0xD8, 0x2D, 0x1D, 0x27, 0xBB, 0x34, 0x01, 0x04, 0x20, 0x62, 0xAC, 0xFD, - 0x4E, 0x7F, 0x54, 0xCE, 0x93, 0xDD, 0xF2, 0x6A, 0x4A, 0xE8, 0x7C, 0xF7, 0x15, 0x68, - 0xC1, 0xD4, 0xE8, 0xBB, 0x8F, 0xCA, 0x20, 0xBB, 0x6F, 0x7B, 0xCE, 0x2C, 0x5B, 0x54, - 0x57, 0x6D, 0x31, 0x5B, 0x21, 0xEA, 0xE3, 0x1A, 0x61, 0x46, 0x41, 0xAF, 0xD2, 0x27, - 0xCD, 0xA2, 0x21, 0xFD, 0x6B, 0x1C, 0x54, 0xEA - ] -); - -musig2::verify_single( - aggregated_pubkey, - &final_signature, - message -) -.expect("aggregated signature must be valid"); -``` -
-
- -## Single Aggregator - -As an alternative to a many-to-many topology where each signer must collect nonces and partial signatures from everyone else in the group, the group can instead opt to nominate an _aggregator node_ whose duty is to collect nonces and signatures from all other signers, and then broadcast the aggregated signature once they receive all partial signatures. - -This dramatically decreases the number of network round-trips required for large groups of signers, and doesn't require any trust in the aggregator node beyond the possibility that they may refuse to reveal the final signature. - -Here's an example of how to use the State-Machine API to interact with an untrusted remote aggregator node. - -
-

Example

- -```rust -# #[cfg(feature = "secp256k1")] -# use secp256k1::{SecretKey, PublicKey}; -# #[cfg(not(feature = "secp256k1"))] -# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; -# use musig2::KeyAggContext; -# -# /// Same pubkeys as in previous example -# let key_agg_ctx = -# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ -# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ -# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" -# .parse::() -# .unwrap(); -# -# let signer_index = 2; -# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" -# .parse() -# .unwrap(); -# -# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); -# -use musig2::{ - AggNonce, FirstRound, PartialSignature, PubNonce, SecNonceSpices, SecondRound, -}; - -let message = "hello interwebz!"; - -// Normally this should be sampled securely from a CSPRNG. -let nonce_seed = [0xACu8; 32]; - -let first_round = FirstRound::new( - key_agg_ctx.clone(), - nonce_seed, - signer_index, - SecNonceSpices::new() - .with_seckey(seckey) - .with_message(&message), -) -.unwrap(); - -// We would share our public nonce with the aggregator. -// The aggregator aggregates the group's nonces together -// and sends us the resulting `AggNonce`. -let aggregated_nonce = AggNonce::sum([ - "02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ - 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45" - .parse::() - .unwrap(), - - "020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ - 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d" - .parse::() - .unwrap(), - - first_round.our_public_nonce(), -]); - -// Once we have the aggregated nonce, we can sign the message, -// and send the partial signature to the aggregator. -let our_partial_signature = first_round - .sign_for_aggregator(seckey, message, &aggregated_nonce) - .unwrap(); - -let partial_signatures = [ - "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" - .parse::() - .unwrap(), - "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" - .parse::() - .unwrap(), - our_partial_signature, -]; - -// The aggregator aggregates the group's partial signatures, -// either using `SecondRound` or the functional API. -let final_signature: [u8; 64] = musig2::aggregate_partial_signatures( - &key_agg_ctx, - &aggregated_nonce, - partial_signatures, - message, -) -.unwrap(); - -musig2::verify_single( - aggregated_pubkey, - &final_signature, - message -) -.expect("aggregated signature must be valid"); -``` -
-
- -The partial signatures can also be created using the functional API, as long as `SecNonce` is [managed carefully so that it is not accidentally reused.](#nonce-reuse) - -## Signatures - -Partial signatures are represented as a [`secp::MaybeScalar`], which is just a scalar in the range `[0, n)` (where `n` is the number of points on the curve). This is aliased as [`PartialSignature`] for clarity. `PartialSignature` implements `Serialize` and `Deserialize` if the `serde` feature is enabled. - -The final output of a signature aggregation is a tuple of numbers `(R, s)` where `R` is a point and `s` is a scalar. This output type is represented by the [`LiftedSignature`] type. The return value of [`SecondRound::finalize`] or [`aggregate_partial_signatures`] can be converted to any type that implements `From`. - -
-

Example

- -```rust -# use musig2::{AggNonce, KeyAggContext, PartialSignature}; -# -# fn main() -> Result<(), Box> { -# /// Same pubkeys as in previous example -# let key_agg_ctx = -# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ -# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ -# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" -# .parse::() -# .unwrap(); -# let message = "hello interwebz!"; -# let partial_signatures = [ -# "5a476e0126583e9e0ceebb01a34bdd342c72eab92efbe8a1c7f07e793fd88f96" -# .parse::() -# .unwrap(), -# "45ac8a698fc9e82408367e28a2d257edf6fc49f14dcc8a98c43e9693e7265e7e" -# .parse::() -# .unwrap(), -# "efd62850b959a76a462f1e42eb3cecc77a5a0982742fff2901456b7d1453a817" -# .parse::() -# .unwrap(), -# ]; -# let aggregated_nonce = "03f9ce0458831f7f8104f014d940db4048c4e045c369c207ec38530360ce7bfd3e\ -# 023f5d6a34513458188503e7c48c1a6efd75f52e77da57587f372be8f839ecc1f9" -# .parse::() -# .unwrap(); -# -use musig2::{aggregate_partial_signatures, CompactSignature, LiftedSignature}; - -// Represents a compacted signature with an X-only nonce point. -let final_signature: CompactSignature = aggregate_partial_signatures( - // ... -# &key_agg_ctx, -# &aggregated_nonce, -# partial_signatures, -# message, -)?; - -// Represents a fully parsed `(R, s)` signature pair. -let final_signature: LiftedSignature = aggregate_partial_signatures( - // ... -# &key_agg_ctx, -# &aggregated_nonce, -# partial_signatures, -# message, -)?; - -// Or you can convert it directly to a byte array. -let final_signature: [u8; 64] = aggregate_partial_signatures( - // ... -# &key_agg_ctx, -# &aggregated_nonce, -# partial_signatures, -# message, -)?; - -# #[cfg(feature = "secp256k1")] -let final_signature: secp256k1::schnorr::Signature = aggregate_partial_signatures( - // ... -# &key_agg_ctx, -# &aggregated_nonce, -# partial_signatures, -# message, -)?; - -// allows us to use `R` as a variable name in this block -#[allow(non_snake_case)] -{ - // You can also unzip signatures into their individual components `(R, s)`. - let signature: LiftedSignature = aggregate_partial_signatures( - // ... - # &key_agg_ctx, - # &aggregated_nonce, - # partial_signatures, - # message, - )?; - - // `R` can be any type that impls `From`. - // `s` can be any type that impls `From`. - let (R, s): (secp::Point, secp::MaybeScalar) = signature.unzip(); - let (R, s): ([u8; 33], [u8; 32]) = signature.unzip(); - # #[cfg(feature = "secp256k1")] - let (R, s): (secp256k1::PublicKey, secp::MaybeScalar) = signature.unzip(); - # #[cfg(feature = "k256")] - let (R, s): (k256::PublicKey, k256::Scalar) = signature.unzip(); - # #[cfg(feature = "k256")] - let (R, s): (k256::AffinePoint, k256::Scalar) = signature.unzip(); -} -# -# Ok(()) -# } -``` -
-
- -This crate exports [BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)-compatible compact Schnorr signature functionality as well. - -- [`verify_single`] - Single Schnorr signature verification. -- [`verify_batch`] - Efficient batched signature verification. -- [`sign_solo`] - Single-key message signing. - -## Serialization - -Binary and hex serialization with is implemented for the following types. - -- [`KeyAggContext`] -- [`SecNonce`] -- [`PubNonce`] -- [`AggNonce`] -- [`LiftedSignature`] -- [`CompactSignature`] - -This is accomplished through the [`BinaryEncoding`] trait. Aliases to the methods of [`BinaryEncoding`] are declared on the vanilla implementations of each type. In addition, these types all implement common standard library traits: - -- [`std::fmt::LowerHex`] -- [`std::fmt::UpperHex`] -- [`std::str::FromStr`] -- [`std::convert::TryFrom<&[u8]>`][std::convert::TryFrom] -- [`std::convert::TryFrom<[u8; N]>`][std::convert::TryFrom] (except [`KeyAggContext`]) -- [`std::convert::TryFrom<&[u8; N]>`][std::convert::TryFrom] (except [`KeyAggContext`]) - -They can also be infallibly converted to [`Vec`][Vec] using [`std::convert::From`], or to `[u8; N]` for fixed-length encodable types. - -If the `serde` feature is enabled, the above types implement [`serde::Serialize`] and [`serde::Deserialize`] for both binary and hex representations in constant time using the [`serdect`] crate. - -
-

Example

- -```rust -# #[cfg(feature = "serde")] -# { -use musig2::{KeyAggContext, PubNonce, SecNonce}; - -#[derive(serde::Deserialize)] -struct CustomSigningSession { - key_agg_ctx: KeyAggContext, - pubnonces: Vec, - secnonce: SecNonce, - message: String, -} - -let json_data = "{ - \"key_agg_ctx\": \"034191a1714ff295b6bc1008aaab813ac5c47bb7d4e64065c0d488b35ead12e0ba\ - 000000020355d7de59c20355f9d8b14ccb60998983e9d73b38f64c9b9f2c4f868c\ - 0a7ac02f039582f6f17f99784bc6de6e5664ef5f69eb1bf0dc151d824b19481ab0717c0cd5\", - \"pubnonces\": [ - \"02af252206259fc1bf588b1f847e15ac78fa840bfb06014cdbddcfcc0e5876f9c9\ - 0380ab2fc9abe84ef42a8d87062d5094b9ab03f4150003a5449846744a49394e45\", - \"020ab52d58f00887d5082c41dc85fd0bd3aaa108c2c980e0337145ac7003c28812\ - 03956ec5bd53023261e982ac0c6f5f2e4b6c1e14e9b1992fb62c9bdfcf5b27dc8d\" - ], - \"secnonce\": \"B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB64\ - 95B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2\", - \"message\": \"attack at dawn\" -}"; - -let session: CustomSigningSession = serde_json::from_str(json_data).unwrap(); - -use musig2::BinaryEncoding; - -let key_agg_bytes: Vec = session.key_agg_ctx.to_bytes(); -let first_pubnonce_bytes: [u8; 66] = session.pubnonces[0].to_bytes(); -let secnonce_bytes = <[u8; 64]>::from(session.secnonce); - -let decoded_key_agg_ctx = KeyAggContext::from_bytes(&key_agg_bytes).unwrap(); -let decoded_pubnonce = PubNonce::try_from(&first_pubnonce_bytes).unwrap(); -let decoded_secnonce = SecNonce::try_from(secnonce_bytes).unwrap(); -# } -``` -
-
- -# Security - -## Nonce Reuse - -The easiest pitfall for downstream instantiations of the MuSig2 protocol is accidental nonce reuse. If you ever reuse a [`SecNonce`] for two different signing sessions, [a co-signer can trick you into exposing your private key](https://medium.com/blockstream/musig-dn-schnorr-multisignatures-with-verifiably-deterministic-nonces-27424b5df9d6#e3b6). - -
-

But how?

- -The malicious co-signer opens two signing sessions on the same message, and provides different nonces to the victim in both sessions. Even if the victim reuses _their_ secret nonce, a different nonce from a co-signer will result in different _aggregated_ nonces `R` and `R'` for both signing sessions. See [the Nonce Generation Algorithm in BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki#user-content-nonce-aggregation) for more details on why this is. - -The challenge hash `e` is computed as `e = H(R, Q, m)` (where `Q` is the aggregated pubkey and `m` is the message). Since the aggregated nonce `R'` of the second session is different, this results in a new challenge hash `e' = H(R', Q, m)` for the second signing session. - -The victim's partial signatures `s` and `s'` for both signing sessions would be computed as: - -```notrust -s = k + e * a * d -s' = k + e' * a * d -``` - -...Where `d` is their secret key, `a` is a publicly known key-coefficient, and `k` is their secret nonce. - -Given both `s` and `s'` from the victim, the attacker can then solve for and compute the victim's private key `d`. - -```notrust -k = s - e * a * d -s' = k + e' * a * d -s' = s - e * a * d + e' * a * d -s' = s - a * d * (e + e') -a * d * (e + e') = s - s' -d = (s - s') / a * (e + e') -``` -
-
- - -The [State-Machine API](#state-machine-api) is designed to avoid this possibility by computing and storing the [`SecNonce`] inside the [`FirstRound`] struct, and never exposing it directly to the downstream consumer. - -When using the `FirstRound` API, we recommend enabling the `rand` feature on this crate, and passing [`&mut rand::rngs::OsRng`][rand::rngs::OsRng] or [`&mut rand::thread_rng()`][rand::thread_rng] as the `nonce_seed` argument to [`FirstRound::new`]. This reduces the risk of accidental nonce reuse significantly. - -
-

Example

- -```rust -# #[cfg(feature = "secp256k1")] -# use secp256k1::{SecretKey, PublicKey}; -# #[cfg(not(feature = "secp256k1"))] -# use musig2::secp::{Point as PublicKey, Scalar as SecretKey}; -# use musig2::KeyAggContext; -# -# /// Same pubkeys as in previous example -# let key_agg_ctx = -# "0000000003026e14224899cf9c780fef5dd200f92a28cc67f71c0af6fe30b5657ffc943f08f402f3\ -# b071c064f115ca762ed88c3efd1927ea657c7949698b77255ea25751331f0b03204ea8bc3425b2cb\ -# c9cb20617f67dc6b202467591d0b26d059e370b71ee392eb" -# .parse::() -# .unwrap(); -# -# let signer_index = 2; -# let seckey: SecretKey = "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" -# .parse() -# .unwrap(); -# -# let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); -# -# // The group wants to sign something! -# let message = "hello interwebz!"; -use musig2::{FirstRound, SecNonceSpices}; - -# #[cfg(feature = "rand")] -let mut first_round = FirstRound::new( - key_agg_ctx, - &mut rand::rngs::OsRng, - signer_index, - SecNonceSpices::new() - .with_seckey(seckey) - .with_message(&message), -) -.unwrap(); -``` - -
- -If you decide to use the Functional API instead for any reason, **you must ensure your code is adequately protected against accidental nonce reuse.** - -## Constant Time Operations - -All sensitive operations in this library endeavor to act in constant-time, independent of secret input. We mostly depend on the upstream [`k256`] and [`secp256k1`] crates for this functionality though, and no independent testing has confirmed this yet. diff --git a/crates/musig2/doc/adaptor_signatures.md b/crates/musig2/doc/adaptor_signatures.md deleted file mode 100644 index d82213e8..00000000 --- a/crates/musig2/doc/adaptor_signatures.md +++ /dev/null @@ -1,246 +0,0 @@ -This module exports [adaptor signature](https://bitcoinops.org/en/topics/adaptor-signatures/) implementations of BIP340 signing and MuSig signing. - -Adaptor signatures allow signers to create Schnorr signatures which can be verified, but do not pass BIP340 verification logic unless a specific secret scalar is added to the signature. - -[Further reading](https://conduition.io/scriptless/adaptorsigs/). - -## MuSig Example - -Here we demonstrate a group of MuSig2 signers adaptor-signing the same message. The final signature which the group constructs is an [`AdaptorSignature`], which they cannot use until it has been decrypted (AKA 'adapted') by the correct adaptor secret (a scalar). - -```rust -use secp::{MaybeScalar, Point, Scalar}; -use musig2::{AdaptorSignature, KeyAggContext, PartialSignature, PubNonce}; - -let seckeys = [ - Scalar::from_slice(&[0x11; 32]).unwrap(), - Scalar::from_slice(&[0x22; 32]).unwrap(), - Scalar::from_slice(&[0x33; 32]).unwrap(), -]; - -let pubkeys = [ - seckeys[0].base_point_mul(), - seckeys[1].base_point_mul(), - seckeys[2].base_point_mul(), -]; - -let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); -let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); - -let message = "danger, will robinson!"; - -let adaptor_secret = Scalar::random(&mut rand::thread_rng()); -let adaptor_point = adaptor_secret.base_point_mul(); - -// Using the functional API. -{ - use musig2::{AggNonce, SecNonce}; - - let secnonces = [ - SecNonce::build([0x11; 32]).build(), - SecNonce::build([0x22; 32]).build(), - SecNonce::build([0x33; 32]).build(), - ]; - - let pubnonces = [ - secnonces[0].public_nonce(), - secnonces[1].public_nonce(), - secnonces[2].public_nonce(), - ]; - - let aggnonce = AggNonce::sum(&pubnonces); - - let partial_signatures: Vec = seckeys - .into_iter() - .zip(secnonces) - .map(|(seckey, secnonce)| { - musig2::adaptor::sign_partial( - &key_agg_ctx, - seckey, - secnonce, - &aggnonce, - adaptor_point, - &message, - ) - }) - .collect::, _>>() - .expect("failed to create partial adaptor signatures"); - - let adaptor_signature: AdaptorSignature = musig2::adaptor::aggregate_partial_signatures( - &key_agg_ctx, - &aggnonce, - adaptor_point, - partial_signatures.iter().copied(), - &message, - ) - .expect("failed to aggregate partial adaptor signatures"); - - // Verify the adaptor signature is valid for the given adaptor point and pubkey. - musig2::adaptor::verify_single( - aggregated_pubkey, - &adaptor_signature, - &message, - adaptor_point, - ) - .expect("invalid aggregated adaptor signature"); - - // Decrypt the signature with the adaptor secret. - let valid_signature = adaptor_signature.adapt(adaptor_secret).unwrap(); - - musig2::verify_single( - aggregated_pubkey, - valid_signature, - &message, - ) - .expect("invalid decrypted adaptor signature"); - - // The decrypted signature and the adaptor signature allow an - // observer to deduce the adaptor secret. - let revealed: MaybeScalar = adaptor_signature - .reveal_secret(&valid_signature) - .expect("should compute adaptor secret from decrypted signature"); - - assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); -} - -// Using the state-machine API -{ - use musig2::{FirstRound, SecNonceSpices, SecondRound}; - - let spiced = |i| SecNonceSpices::new() - .with_seckey(seckeys[i]) - .with_message(&message); - - let mut first_rounds = vec![ - FirstRound::new(key_agg_ctx.clone(), [0x11; 32], 0, spiced(0)).unwrap(), - FirstRound::new(key_agg_ctx.clone(), [0x22; 32], 1, spiced(1)).unwrap(), - FirstRound::new(key_agg_ctx.clone(), [0x33; 32], 2, spiced(2)).unwrap(), - ]; - - let public_nonces = [ - first_rounds[0].our_public_nonce(), - first_rounds[1].our_public_nonce(), - first_rounds[2].our_public_nonce(), - ]; - - for round in first_rounds.iter_mut() { - round.receive_nonce(0, public_nonces[0].clone()).unwrap(); - round.receive_nonce(1, public_nonces[1].clone()).unwrap(); - round.receive_nonce(2, public_nonces[2].clone()).unwrap(); - } - - // The `finalize_adaptor` method must be used instead of `finalize`, on - // both first and second rounds. - let mut second_rounds: Vec> = first_rounds - .into_iter() - .enumerate() - .map(|(i, round)| round.finalize_adaptor(seckeys[i], adaptor_point, message).unwrap()) - .collect(); - - let partial_sigs: [PartialSignature; 3] = [ - second_rounds[0].our_signature(), - second_rounds[1].our_signature(), - second_rounds[2].our_signature(), - ]; - - for round in second_rounds.iter_mut() { - round.receive_signature(0, partial_sigs[0]).unwrap(); - round.receive_signature(1, partial_sigs[1]).unwrap(); - round.receive_signature(2, partial_sigs[2]).unwrap(); - } - - for second_round in second_rounds.into_iter() { - let adaptor_signature = second_round.finalize_adaptor::().unwrap(); - - // Verify the adaptor signature is valid for the given adaptor point and pubkey. - musig2::adaptor::verify_single( - aggregated_pubkey, - &adaptor_signature, - &message, - adaptor_point, - ) - .expect("invalid aggregated adaptor signature"); - - // Decrypt the signature with the adaptor secret. - let valid_signature = adaptor_signature.adapt(adaptor_secret).unwrap(); - musig2::verify_single( - aggregated_pubkey, - valid_signature, - &message, - ) - .expect("invalid decrypted adaptor signature"); - - // The decrypted signature and the adaptor signature allow an - // observer to deduce the adaptor secret. - let revealed: MaybeScalar = adaptor_signature - .reveal_secret(&valid_signature) - .expect("should compute adaptor secret from decrypted signature"); - - assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); - } -} -``` - -## Single Signer Example - -We also export single-signer adaptor signing logic. - -```rust -use secp::{MaybeScalar, Scalar}; - -let seckey = Scalar::random(&mut rand::rngs::OsRng); -let message = "hello world!"; - -// Create an adaptor signature, encrypted under a specific adaptor point. -let adaptor_secret = Scalar::random(&mut rand::rngs::OsRng); -let adaptor_point = adaptor_secret.base_point_mul(); -let aux_rand = [0xAA; 32]; // Should use an actual RNG. -let adaptor_signature = - musig2::adaptor::sign_solo(seckey, message, aux_rand, adaptor_point); - -// Verify the adaptor signature is valid for the given adaptor point and pubkey. -let pubkey = seckey.base_point_mul(); -musig2::adaptor::verify_single(pubkey, &adaptor_signature, message, adaptor_point) - .expect("valid adaptor signature should verify"); - -// Decrypt the signature with the adaptor secret. -let valid_sig: musig2::LiftedSignature = adaptor_signature - .adapt(adaptor_secret) - .expect("invalid adaptor secret"); - -musig2::verify_single(pubkey, valid_sig, message) - .expect("decrypted adaptor signature is valid"); - -// The decrypted signature and the adaptor signature allow an -// observer to deduce the adaptor secret. -let revealed: MaybeScalar = adaptor_signature.reveal_secret(&valid_sig) - .expect("decrypted sig should reveal adaptor secret"); -assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); -``` - -## Encrypting Signatures - -The above examples create signatures by committing to an adaptor point as part of the signing process. This is the way most adaptor signatures are created. - -However, you can also encrypt existing signatures (including existing adaptor signatures) by tweaking them with an adaptor secret. This requires knowing the adaptor secret though, so you can't do this if you only know the public adaptor point. - -```rust -let signature = musig2::LiftedSignature::from_hex( - "e565f19755422162cf7dc69ed8a4f4a27d81363d024a3de355644003da33ed2f\ - 0cdd95945c6d28841192867842c104391b9cc31f25706ee302a96204a1d43eb7" -) -.unwrap(); - -let adaptor_secret_1 = secp::Scalar::from_slice(&[0x55; 32]).unwrap(); -let mut adaptor_signature: musig2::AdaptorSignature = signature.encrypt(adaptor_secret_1); - -// We can re-encrypt the same adaptor signature twice, so that it is locked behind -// two different points. Both secrets must be learned to compute the valid signature. -let adaptor_secret_2 = secp::Scalar::from_slice(&[0x66; 32]).unwrap(); -adaptor_signature = adaptor_signature.encrypt(adaptor_secret_2); - -let decrypted: [u8; 64] = adaptor_signature - .adapt(adaptor_secret_1 + adaptor_secret_2) - .expect("valid decrypted adaptor signature"); -assert_eq!(decrypted, signature.serialize()); -``` diff --git a/crates/musig2/reference.py b/crates/musig2/reference.py deleted file mode 100644 index b2d0436b..00000000 --- a/crates/musig2/reference.py +++ /dev/null @@ -1,882 +0,0 @@ -# Source: https://github.com/bitcoin/bips/blob/master/bip-0327/reference.py -# -# BIP327 reference implementation -# -# WARNING: This implementation is for demonstration purposes only and _not_ to -# be used in production environments. The code is vulnerable to timing attacks, -# for example. - -from typing import Any, List, Optional, Tuple, NewType, NamedTuple -import hashlib -import secrets -import time - -# -# The following helper functions were copied from the BIP-340 reference implementation: -# https://github.com/bitcoin/bips/blob/master/bip-0340/reference.py -# - -p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F -n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - -# Points are tuples of X and Y coordinates and the point at infinity is -# represented by the None keyword. -G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) - -Point = Tuple[int, int] - -# This implementation can be sped up by storing the midstate after hashing -# tag_hash instead of rehashing it all the time. -def tagged_hash(tag: str, msg: bytes) -> bytes: - tag_hash = hashlib.sha256(tag.encode()).digest() - return hashlib.sha256(tag_hash + tag_hash + msg).digest() - -def is_infinite(P: Optional[Point]) -> bool: - return P is None - -def x(P: Point) -> int: - assert not is_infinite(P) - return P[0] - -def y(P: Point) -> int: - assert not is_infinite(P) - return P[1] - -def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: - if P1 is None: - return P2 - if P2 is None: - return P1 - if (x(P1) == x(P2)) and (y(P1) != y(P2)): - return None - if P1 == P2: - lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p - else: - lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p - x3 = (lam * lam - x(P1) - x(P2)) % p - return (x3, (lam * (x(P1) - x3) - y(P1)) % p) - -def point_mul(P: Optional[Point], n: int) -> Optional[Point]: - R = None - for i in range(256): - if (n >> i) & 1: - R = point_add(R, P) - P = point_add(P, P) - return R - -def bytes_from_int(x: int) -> bytes: - return x.to_bytes(32, byteorder="big") - -def lift_x(b: bytes) -> Optional[Point]: - x = int_from_bytes(b) - if x >= p: - return None - y_sq = (pow(x, 3, p) + 7) % p - y = pow(y_sq, (p + 1) // 4, p) - if pow(y, 2, p) != y_sq: - return None - return (x, y if y & 1 == 0 else p-y) - -def int_from_bytes(b: bytes) -> int: - return int.from_bytes(b, byteorder="big") - -def has_even_y(P: Point) -> bool: - assert not is_infinite(P) - return y(P) % 2 == 0 - -def schnorr_verify(msg: bytes, pubkey: bytes, sig: bytes) -> bool: - if len(msg) != 32: - raise ValueError('The message must be a 32-byte array.') - if len(pubkey) != 32: - raise ValueError('The public key must be a 32-byte array.') - if len(sig) != 64: - raise ValueError('The signature must be a 64-byte array.') - P = lift_x(pubkey) - r = int_from_bytes(sig[0:32]) - s = int_from_bytes(sig[32:64]) - if (P is None) or (r >= p) or (s >= n): - return False - e = int_from_bytes(tagged_hash("BIP0340/challenge", sig[0:32] + pubkey + msg)) % n - R = point_add(point_mul(G, s), point_mul(P, n - e)) - if (R is None) or (not has_even_y(R)) or (x(R) != r): - return False - return True - -# -# End of helper functions copied from BIP-340 reference implementation. -# - -PlainPk = NewType('PlainPk', bytes) -XonlyPk = NewType('XonlyPk', bytes) - -# There are two types of exceptions that can be raised by this implementation: -# - ValueError for indicating that an input doesn't conform to some function -# precondition (e.g. an input array is the wrong length, a serialized -# representation doesn't have the correct format). -# - InvalidContributionError for indicating that a signer (or the -# aggregator) is misbehaving in the protocol. -# -# Assertions are used to (1) satisfy the type-checking system, and (2) check for -# inconvenient events that can't happen except with negligible probability (e.g. -# output of a hash function is 0) and can't be manually triggered by any -# signer. - -# This exception is raised if a party (signer or nonce aggregator) sends invalid -# values. Actual implementations should not crash when receiving invalid -# contributions. Instead, they should hold the offending party accountable. -class InvalidContributionError(Exception): - def __init__(self, signer, contrib): - self.signer = signer - # contrib is one of "pubkey", "pubnonce", "aggnonce", or "psig". - self.contrib = contrib - -infinity = None - -def xbytes(P: Point) -> bytes: - return bytes_from_int(x(P)) - -def cbytes(P: Point) -> bytes: - a = b'\x02' if has_even_y(P) else b'\x03' - return a + xbytes(P) - -def cbytes_ext(P: Optional[Point]) -> bytes: - if is_infinite(P): - return (0).to_bytes(33, byteorder='big') - assert P is not None - return cbytes(P) - -def point_negate(P: Optional[Point]) -> Optional[Point]: - if P is None: - return P - return (x(P), p - y(P)) - -def cpoint(x: bytes) -> Point: - if len(x) != 33: - raise ValueError('x is not a valid compressed point.') - P = lift_x(x[1:33]) - if P is None: - raise ValueError('x is not a valid compressed point.') - if x[0] == 2: - return P - elif x[0] == 3: - P = point_negate(P) - assert P is not None - return P - else: - raise ValueError('x is not a valid compressed point.') - -def cpoint_ext(x: bytes) -> Optional[Point]: - if x == (0).to_bytes(33, 'big'): - return None - else: - return cpoint(x) - -# Return the plain public key corresponding to a given secret key -def individual_pk(seckey: bytes) -> PlainPk: - d0 = int_from_bytes(seckey) - if not (1 <= d0 <= n - 1): - raise ValueError('The secret key must be an integer in the range 1..n-1.') - P = point_mul(G, d0) - assert P is not None - return PlainPk(cbytes(P)) - -def key_sort(pubkeys: List[PlainPk]) -> List[PlainPk]: - pubkeys.sort() - return pubkeys - -KeyAggContext = NamedTuple('KeyAggContext', [('Q', Point), - ('gacc', int), - ('tacc', int)]) - -def get_xonly_pk(keyagg_ctx: KeyAggContext) -> XonlyPk: - Q, _, _ = keyagg_ctx - return XonlyPk(xbytes(Q)) - -def key_agg(pubkeys: List[PlainPk]) -> KeyAggContext: - pk2 = get_second_key(pubkeys) - u = len(pubkeys) - Q = infinity - for i in range(u): - try: - P_i = cpoint(pubkeys[i]) - except ValueError: - raise InvalidContributionError(i, "pubkey") - a_i = key_agg_coeff_internal(pubkeys, pubkeys[i], pk2) - Q = point_add(Q, point_mul(P_i, a_i)) - # Q is not the point at infinity except with negligible probability. - assert(Q is not None) - gacc = 1 - tacc = 0 - return KeyAggContext(Q, gacc, tacc) - -def hash_keys(pubkeys: List[PlainPk]) -> bytes: - return tagged_hash('KeyAgg list', b''.join(pubkeys)) - -def get_second_key(pubkeys: List[PlainPk]) -> PlainPk: - u = len(pubkeys) - for j in range(1, u): - if pubkeys[j] != pubkeys[0]: - return pubkeys[j] - return PlainPk(b'\x00'*33) - -def key_agg_coeff(pubkeys: List[PlainPk], pk_: PlainPk) -> int: - pk2 = get_second_key(pubkeys) - return key_agg_coeff_internal(pubkeys, pk_, pk2) - -def key_agg_coeff_internal(pubkeys: List[PlainPk], pk_: PlainPk, pk2: PlainPk) -> int: - L = hash_keys(pubkeys) - if pk_ == pk2: - return 1 - return int_from_bytes(tagged_hash('KeyAgg coefficient', L + pk_)) % n - -def apply_tweak(keyagg_ctx: KeyAggContext, tweak: bytes, is_xonly: bool) -> KeyAggContext: - if len(tweak) != 32: - raise ValueError('The tweak must be a 32-byte array.') - Q, gacc, tacc = keyagg_ctx - if is_xonly and not has_even_y(Q): - g = n - 1 - else: - g = 1 - t = int_from_bytes(tweak) - if t >= n: - raise ValueError('The tweak must be less than n.') - Q_ = point_add(point_mul(Q, g), point_mul(G, t)) - if Q_ is None: - raise ValueError('The result of tweaking cannot be infinity.') - gacc_ = g * gacc % n - tacc_ = (t + g * tacc) % n - return KeyAggContext(Q_, gacc_, tacc_) - -def bytes_xor(a: bytes, b: bytes) -> bytes: - return bytes(x ^ y for x, y in zip(a, b)) - -def nonce_hash(rand: bytes, pk: PlainPk, aggpk: XonlyPk, i: int, msg_prefixed: bytes, extra_in: bytes) -> int: - buf = b'' - buf += rand - buf += len(pk).to_bytes(1, 'big') - buf += pk - buf += len(aggpk).to_bytes(1, 'big') - buf += aggpk - buf += msg_prefixed - buf += len(extra_in).to_bytes(4, 'big') - buf += extra_in - buf += i.to_bytes(1, 'big') - return int_from_bytes(tagged_hash('MuSig/nonce', buf)) - -def nonce_gen_internal(rand_: bytes, sk: Optional[bytes], pk: PlainPk, aggpk: Optional[XonlyPk], msg: Optional[bytes], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: - if sk is not None: - rand = bytes_xor(sk, tagged_hash('MuSig/aux', rand_)) - else: - rand = rand_ - if aggpk is None: - aggpk = XonlyPk(b'') - if msg is None: - msg_prefixed = b'\x00' - else: - msg_prefixed = b'\x01' - msg_prefixed += len(msg).to_bytes(8, 'big') - msg_prefixed += msg - if extra_in is None: - extra_in = b'' - k_1 = nonce_hash(rand, pk, aggpk, 0, msg_prefixed, extra_in) % n - k_2 = nonce_hash(rand, pk, aggpk, 1, msg_prefixed, extra_in) % n - # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. - assert k_1 != 0 - assert k_2 != 0 - R_s1 = point_mul(G, k_1) - R_s2 = point_mul(G, k_2) - assert R_s1 is not None - assert R_s2 is not None - pubnonce = cbytes(R_s1) + cbytes(R_s2) - secnonce = bytearray(bytes_from_int(k_1) + bytes_from_int(k_2) + pk) - return secnonce, pubnonce - -def nonce_gen(sk: Optional[bytes], pk: PlainPk, aggpk: Optional[XonlyPk], msg: Optional[bytes], extra_in: Optional[bytes]) -> Tuple[bytearray, bytes]: - if sk is not None and len(sk) != 32: - raise ValueError('The optional byte array sk must have length 32.') - if aggpk is not None and len(aggpk) != 32: - raise ValueError('The optional byte array aggpk must have length 32.') - rand_ = secrets.token_bytes(32) - return nonce_gen_internal(rand_, sk, pk, aggpk, msg, extra_in) - -def nonce_agg(pubnonces: List[bytes]) -> bytes: - u = len(pubnonces) - aggnonce = b'' - for j in (1, 2): - R_j = infinity - for i in range(u): - try: - R_ij = cpoint(pubnonces[i][(j-1)*33:j*33]) - except ValueError: - raise InvalidContributionError(i, "pubnonce") - R_j = point_add(R_j, R_ij) - aggnonce += cbytes_ext(R_j) - return aggnonce - -SessionContext = NamedTuple('SessionContext', [('aggnonce', bytes), - ('pubkeys', List[PlainPk]), - ('tweaks', List[bytes]), - ('is_xonly', List[bool]), - ('msg', bytes)]) - -def key_agg_and_tweak(pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool]): - if len(tweaks) != len(is_xonly): - raise ValueError('The `tweaks` and `is_xonly` arrays must have the same length.') - keyagg_ctx = key_agg(pubkeys) - v = len(tweaks) - for i in range(v): - keyagg_ctx = apply_tweak(keyagg_ctx, tweaks[i], is_xonly[i]) - return keyagg_ctx - -def get_session_values(session_ctx: SessionContext) -> Tuple[Point, int, int, int, Point, int]: - (aggnonce, pubkeys, tweaks, is_xonly, msg) = session_ctx - Q, gacc, tacc = key_agg_and_tweak(pubkeys, tweaks, is_xonly) - b = int_from_bytes(tagged_hash('MuSig/noncecoef', aggnonce + xbytes(Q) + msg)) % n - try: - R_1 = cpoint_ext(aggnonce[0:33]) - R_2 = cpoint_ext(aggnonce[33:66]) - except ValueError: - # Nonce aggregator sent invalid nonces - raise InvalidContributionError(None, "aggnonce") - R_ = point_add(R_1, point_mul(R_2, b)) - R = R_ if not is_infinite(R_) else G - assert R is not None - e = int_from_bytes(tagged_hash('BIP0340/challenge', xbytes(R) + xbytes(Q) + msg)) % n - return (Q, gacc, tacc, b, R, e) - -def get_session_key_agg_coeff(session_ctx: SessionContext, P: Point) -> int: - (_, pubkeys, _, _, _) = session_ctx - pk = PlainPk(cbytes(P)) - if pk not in pubkeys: - raise ValueError('The signer\'s pubkey must be included in the list of pubkeys.') - return key_agg_coeff(pubkeys, pk) - -def sign(secnonce: bytearray, sk: bytes, session_ctx: SessionContext) -> bytes: - (Q, gacc, _, b, R, e) = get_session_values(session_ctx) - k_1_ = int_from_bytes(secnonce[0:32]) - k_2_ = int_from_bytes(secnonce[32:64]) - # Overwrite the secnonce argument with zeros such that subsequent calls of - # sign with the same secnonce raise a ValueError. - secnonce[:64] = bytearray(b'\x00'*64) - if not 0 < k_1_ < n: - raise ValueError('first secnonce value is out of range.') - if not 0 < k_2_ < n: - raise ValueError('second secnonce value is out of range.') - k_1 = k_1_ if has_even_y(R) else n - k_1_ - k_2 = k_2_ if has_even_y(R) else n - k_2_ - d_ = int_from_bytes(sk) - if not 0 < d_ < n: - raise ValueError('secret key value is out of range.') - P = point_mul(G, d_) - assert P is not None - pk = cbytes(P) - if not pk == secnonce[64:97]: - raise ValueError('Public key does not match nonce_gen argument') - a = get_session_key_agg_coeff(session_ctx, P) - g = 1 if has_even_y(Q) else n - 1 - d = g * gacc * d_ % n - s = (k_1 + b * k_2 + e * a * d) % n - psig = bytes_from_int(s) - R_s1 = point_mul(G, k_1_) - R_s2 = point_mul(G, k_2_) - assert R_s1 is not None - assert R_s2 is not None - pubnonce = cbytes(R_s1) + cbytes(R_s2) - # Optional correctness check. The result of signing should pass signature verification. - assert partial_sig_verify_internal(psig, pubnonce, pk, session_ctx) - return psig - -def det_nonce_hash(sk_: bytes, aggothernonce: bytes, aggpk: bytes, msg: bytes, i: int) -> int: - buf = b'' - buf += sk_ - buf += aggothernonce - buf += aggpk - buf += len(msg).to_bytes(8, 'big') - buf += msg - buf += i.to_bytes(1, 'big') - return int_from_bytes(tagged_hash('MuSig/deterministic/nonce', buf)) - -def deterministic_sign(sk: bytes, aggothernonce: bytes, pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool], msg: bytes, rand: Optional[bytes]) -> Tuple[bytes, bytes]: - if rand is not None: - sk_ = bytes_xor(sk, tagged_hash('MuSig/aux', rand)) - else: - sk_ = sk - aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) - - k_1 = det_nonce_hash(sk_, aggothernonce, aggpk, msg, 0) % n - k_2 = det_nonce_hash(sk_, aggothernonce, aggpk, msg, 1) % n - # k_1 == 0 or k_2 == 0 cannot occur except with negligible probability. - assert k_1 != 0 - assert k_2 != 0 - - R_s1 = point_mul(G, k_1) - R_s2 = point_mul(G, k_2) - assert R_s1 is not None - assert R_s2 is not None - pubnonce = cbytes(R_s1) + cbytes(R_s2) - secnonce = bytearray(bytes_from_int(k_1) + bytes_from_int(k_2) + individual_pk(sk)) - try: - aggnonce = nonce_agg([pubnonce, aggothernonce]) - except Exception: - raise InvalidContributionError(None, "aggothernonce") - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - psig = sign(secnonce, sk, session_ctx) - return (pubnonce, psig) - -def partial_sig_verify(psig: bytes, pubnonces: List[bytes], pubkeys: List[PlainPk], tweaks: List[bytes], is_xonly: List[bool], msg: bytes, i: int) -> bool: - if len(pubnonces) != len(pubkeys): - raise ValueError('The `pubnonces` and `pubkeys` arrays must have the same length.') - if len(tweaks) != len(is_xonly): - raise ValueError('The `tweaks` and `is_xonly` arrays must have the same length.') - aggnonce = nonce_agg(pubnonces) - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - return partial_sig_verify_internal(psig, pubnonces[i], pubkeys[i], session_ctx) - -def partial_sig_verify_internal(psig: bytes, pubnonce: bytes, pk: bytes, session_ctx: SessionContext) -> bool: - (Q, gacc, _, b, R, e) = get_session_values(session_ctx) - s = int_from_bytes(psig) - if s >= n: - return False - R_s1 = cpoint(pubnonce[0:33]) - R_s2 = cpoint(pubnonce[33:66]) - Re_s_ = point_add(R_s1, point_mul(R_s2, b)) - Re_s = Re_s_ if has_even_y(R) else point_negate(Re_s_) - P = cpoint(pk) - if P is None: - return False - a = get_session_key_agg_coeff(session_ctx, P) - g = 1 if has_even_y(Q) else n - 1 - g_ = g * gacc % n - return point_mul(G, s) == point_add(Re_s, point_mul(P, e * a * g_ % n)) - -def partial_sig_agg(psigs: List[bytes], session_ctx: SessionContext) -> bytes: - (Q, _, tacc, _, R, e) = get_session_values(session_ctx) - s = 0 - u = len(psigs) - for i in range(u): - s_i = int_from_bytes(psigs[i]) - if s_i >= n: - raise InvalidContributionError(i, "psig") - s = (s + s_i) % n - g = 1 if has_even_y(Q) else n - 1 - s = (s + e * g * tacc) % n - return xbytes(R) + bytes_from_int(s) -# -# The following code is only used for testing. -# - -import json -import os -import sys - -def fromhex_all(l): - return [bytes.fromhex(l_i) for l_i in l] - -# Check that calling `try_fn` raises a `exception`. If `exception` is raised, -# examine it with `except_fn`. -def assert_raises(exception, try_fn, except_fn): - raised = False - try: - try_fn() - except exception as e: - raised = True - assert(except_fn(e)) - except BaseException: - raise AssertionError("Wrong exception raised in a test.") - if not raised: - raise AssertionError("Exception was _not_ raised in a test where it was required.") - -def get_error_details(test_case): - error = test_case["error"] - if error["type"] == "invalid_contribution": - exception = InvalidContributionError - if "contrib" in error: - except_fn = lambda e: e.signer == error["signer"] and e.contrib == error["contrib"] - else: - except_fn = lambda e: e.signer == error["signer"] - elif error["type"] == "value": - exception = ValueError - except_fn = lambda e: str(e) == error["message"] - else: - raise RuntimeError(f"Invalid error type: {error['type']}") - return exception, except_fn - -def test_key_sort_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'key_sort_vectors.json')) as f: - test_data = json.load(f) - - X = fromhex_all(test_data["pubkeys"]) - X_sorted = fromhex_all(test_data["sorted_pubkeys"]) - - assert key_sort(X) == X_sorted - -def test_key_agg_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'key_agg_vectors.json')) as f: - test_data = json.load(f) - - X = fromhex_all(test_data["pubkeys"]) - T = fromhex_all(test_data["tweaks"]) - valid_test_cases = test_data["valid_test_cases"] - error_test_cases = test_data["error_test_cases"] - - for test_case in valid_test_cases: - pubkeys = [X[i] for i in test_case["key_indices"]] - expected = bytes.fromhex(test_case["expected"]) - - assert get_xonly_pk(key_agg(pubkeys)) == expected - - for i, test_case in enumerate(error_test_cases): - exception, except_fn = get_error_details(test_case) - - pubkeys = [X[i] for i in test_case["key_indices"]] - tweaks = [T[i] for i in test_case["tweak_indices"]] - is_xonly = test_case["is_xonly"] - - assert_raises(exception, lambda: key_agg_and_tweak(pubkeys, tweaks, is_xonly), except_fn) - -def test_nonce_gen_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'nonce_gen_vectors.json')) as f: - test_data = json.load(f) - - for test_case in test_data["test_cases"]: - def get_value(key) -> bytes: - return bytes.fromhex(test_case[key]) - - def get_value_maybe(key) -> Optional[bytes]: - if test_case[key] is not None: - return get_value(key) - else: - return None - - rand_ = get_value("rand_") - sk = get_value_maybe("sk") - pk = PlainPk(get_value("pk")) - aggpk = get_value_maybe("aggpk") - if aggpk is not None: - aggpk = XonlyPk(aggpk) - msg = get_value_maybe("msg") - extra_in = get_value_maybe("extra_in") - expected_secnonce = get_value("expected_secnonce") - expected_pubnonce = get_value("expected_pubnonce") - - assert nonce_gen_internal(rand_, sk, pk, aggpk, msg, extra_in) == (expected_secnonce, expected_pubnonce) - -def test_nonce_agg_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'nonce_agg_vectors.json')) as f: - test_data = json.load(f) - - pnonce = fromhex_all(test_data["pnonces"]) - valid_test_cases = test_data["valid_test_cases"] - error_test_cases = test_data["error_test_cases"] - - for test_case in valid_test_cases: - pubnonces = [pnonce[i] for i in test_case["pnonce_indices"]] - expected = bytes.fromhex(test_case["expected"]) - assert nonce_agg(pubnonces) == expected - - for i, test_case in enumerate(error_test_cases): - exception, except_fn = get_error_details(test_case) - pubnonces = [pnonce[i] for i in test_case["pnonce_indices"]] - assert_raises(exception, lambda: nonce_agg(pubnonces), except_fn) - -def test_sign_verify_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'sign_verify_vectors.json')) as f: - test_data = json.load(f) - - sk = bytes.fromhex(test_data["sk"]) - X = fromhex_all(test_data["pubkeys"]) - # The public key corresponding to sk is at index 0 - assert X[0] == individual_pk(sk) - - secnonces = fromhex_all(test_data["secnonces"]) - pnonce = fromhex_all(test_data["pnonces"]) - # The public nonce corresponding to secnonces[0] is at index 0 - k_1 = int_from_bytes(secnonces[0][0:32]) - k_2 = int_from_bytes(secnonces[0][32:64]) - R_s1 = point_mul(G, k_1) - R_s2 = point_mul(G, k_2) - assert R_s1 is not None and R_s2 is not None - assert pnonce[0] == cbytes(R_s1) + cbytes(R_s2) - - aggnonces = fromhex_all(test_data["aggnonces"]) - # The aggregate of the first three elements of pnonce is at index 0 - assert(aggnonces[0] == nonce_agg([pnonce[0], pnonce[1], pnonce[2]])) - - msgs = fromhex_all(test_data["msgs"]) - - valid_test_cases = test_data["valid_test_cases"] - sign_error_test_cases = test_data["sign_error_test_cases"] - verify_fail_test_cases = test_data["verify_fail_test_cases"] - verify_error_test_cases = test_data["verify_error_test_cases"] - - for test_case in valid_test_cases: - pubkeys = [X[i] for i in test_case["key_indices"]] - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - aggnonce = aggnonces[test_case["aggnonce_index"]] - # Make sure that pubnonces and aggnonce in the test vector are - # consistent - assert nonce_agg(pubnonces) == aggnonce - msg = msgs[test_case["msg_index"]] - signer_index = test_case["signer_index"] - expected = bytes.fromhex(test_case["expected"]) - - session_ctx = SessionContext(aggnonce, pubkeys, [], [], msg) - # WARNING: An actual implementation should _not_ copy the secnonce. - # Reusing the secnonce, as we do here for testing purposes, can leak the - # secret key. - secnonce_tmp = bytearray(secnonces[0]) - assert sign(secnonce_tmp, sk, session_ctx) == expected - assert partial_sig_verify(expected, pubnonces, pubkeys, [], [], msg, signer_index) - - for i, test_case in enumerate(sign_error_test_cases): - exception, except_fn = get_error_details(test_case) - - pubkeys = [X[i] for i in test_case["key_indices"]] - aggnonce = aggnonces[test_case["aggnonce_index"]] - msg = msgs[test_case["msg_index"]] - secnonce = bytearray(secnonces[test_case["secnonce_index"]]) - - session_ctx = SessionContext(aggnonce, pubkeys, [], [], msg) - assert_raises(exception, lambda: sign(secnonce, sk, session_ctx), except_fn) - - for test_case in verify_fail_test_cases: - sig = bytes.fromhex(test_case["sig"]) - pubkeys = [X[i] for i in test_case["key_indices"]] - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - msg = msgs[test_case["msg_index"]] - signer_index = test_case["signer_index"] - - assert not partial_sig_verify(sig, pubnonces, pubkeys, [], [], msg, signer_index) - - for i, test_case in enumerate(verify_error_test_cases): - exception, except_fn = get_error_details(test_case) - - sig = bytes.fromhex(test_case["sig"]) - pubkeys = [X[i] for i in test_case["key_indices"]] - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - msg = msgs[test_case["msg_index"]] - signer_index = test_case["signer_index"] - - assert_raises(exception, lambda: partial_sig_verify(sig, pubnonces, pubkeys, [], [], msg, signer_index), except_fn) - -def test_tweak_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'tweak_vectors.json')) as f: - test_data = json.load(f) - - sk = bytes.fromhex(test_data["sk"]) - X = fromhex_all(test_data["pubkeys"]) - # The public key corresponding to sk is at index 0 - assert X[0] == individual_pk(sk) - - secnonce = bytearray(bytes.fromhex(test_data["secnonce"])) - pnonce = fromhex_all(test_data["pnonces"]) - # The public nonce corresponding to secnonce is at index 0 - k_1 = int_from_bytes(secnonce[0:32]) - k_2 = int_from_bytes(secnonce[32:64]) - R_s1 = point_mul(G, k_1) - R_s2 = point_mul(G, k_2) - assert R_s1 is not None and R_s2 is not None - assert pnonce[0] == cbytes(R_s1) + cbytes(R_s2) - - aggnonce = bytes.fromhex(test_data["aggnonce"]) - # The aggnonce is the aggregate of the first three elements of pnonce - assert(aggnonce == nonce_agg([pnonce[0], pnonce[1], pnonce[2]])) - - tweak = fromhex_all(test_data["tweaks"]) - msg = bytes.fromhex(test_data["msg"]) - - valid_test_cases = test_data["valid_test_cases"] - error_test_cases = test_data["error_test_cases"] - - for test_case in valid_test_cases: - pubkeys = [X[i] for i in test_case["key_indices"]] - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - tweaks = [tweak[i] for i in test_case["tweak_indices"]] - is_xonly = test_case["is_xonly"] - signer_index = test_case["signer_index"] - expected = bytes.fromhex(test_case["expected"]) - - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - secnonce_tmp = bytearray(secnonce) - # WARNING: An actual implementation should _not_ copy the secnonce. - # Reusing the secnonce, as we do here for testing purposes, can leak the - # secret key. - assert sign(secnonce_tmp, sk, session_ctx) == expected - assert partial_sig_verify(expected, pubnonces, pubkeys, tweaks, is_xonly, msg, signer_index) - - for i, test_case in enumerate(error_test_cases): - exception, except_fn = get_error_details(test_case) - - pubkeys = [X[i] for i in test_case["key_indices"]] - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - tweaks = [tweak[i] for i in test_case["tweak_indices"]] - is_xonly = test_case["is_xonly"] - signer_index = test_case["signer_index"] - - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - assert_raises(exception, lambda: sign(secnonce, sk, session_ctx), except_fn) - -def test_det_sign_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'det_sign_vectors.json')) as f: - test_data = json.load(f) - - sk = bytes.fromhex(test_data["sk"]) - X = fromhex_all(test_data["pubkeys"]) - # The public key corresponding to sk is at index 0 - assert X[0] == individual_pk(sk) - - msgs = fromhex_all(test_data["msgs"]) - - valid_test_cases = test_data["valid_test_cases"] - error_test_cases = test_data["error_test_cases"] - - for test_case in valid_test_cases: - pubkeys = [X[i] for i in test_case["key_indices"]] - aggothernonce = bytes.fromhex(test_case["aggothernonce"]) - tweaks = fromhex_all(test_case["tweaks"]) - is_xonly = test_case["is_xonly"] - msg = msgs[test_case["msg_index"]] - signer_index = test_case["signer_index"] - rand = bytes.fromhex(test_case["rand"]) if test_case["rand"] is not None else None - expected = fromhex_all(test_case["expected"]) - - pubnonce, psig = deterministic_sign(sk, aggothernonce, pubkeys, tweaks, is_xonly, msg, rand) - assert pubnonce == expected[0] - assert psig == expected[1] - - pubnonces = [aggothernonce, pubnonce] - aggnonce = nonce_agg(pubnonces) - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - assert partial_sig_verify_internal(psig, pubnonce, pubkeys[signer_index], session_ctx) - - for i, test_case in enumerate(error_test_cases): - exception, except_fn = get_error_details(test_case) - - pubkeys = [X[i] for i in test_case["key_indices"]] - aggothernonce = bytes.fromhex(test_case["aggothernonce"]) - tweaks = fromhex_all(test_case["tweaks"]) - is_xonly = test_case["is_xonly"] - msg = msgs[test_case["msg_index"]] - signer_index = test_case["signer_index"] - rand = bytes.fromhex(test_case["rand"]) if test_case["rand"] is not None else None - - try_fn = lambda: deterministic_sign(sk, aggothernonce, pubkeys, tweaks, is_xonly, msg, rand) - assert_raises(exception, try_fn, except_fn) - -def test_sig_agg_vectors() -> None: - with open(os.path.join(sys.path[0], 'vectors', 'sig_agg_vectors.json')) as f: - test_data = json.load(f) - - X = fromhex_all(test_data["pubkeys"]) - - # These nonces are only required if the tested API takes the individual - # nonces and not the aggregate nonce. - pnonce = fromhex_all(test_data["pnonces"]) - - tweak = fromhex_all(test_data["tweaks"]) - psig = fromhex_all(test_data["psigs"]) - - msg = bytes.fromhex(test_data["msg"]) - - valid_test_cases = test_data["valid_test_cases"] - error_test_cases = test_data["error_test_cases"] - - for test_case in valid_test_cases: - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - aggnonce = bytes.fromhex(test_case["aggnonce"]) - assert aggnonce == nonce_agg(pubnonces) - - pubkeys = [X[i] for i in test_case["key_indices"]] - tweaks = [tweak[i] for i in test_case["tweak_indices"]] - is_xonly = test_case["is_xonly"] - psigs = [psig[i] for i in test_case["psig_indices"]] - expected = bytes.fromhex(test_case["expected"]) - - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - sig = partial_sig_agg(psigs, session_ctx) - assert sig == expected - aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) - assert schnorr_verify(msg, aggpk, sig) - - for i, test_case in enumerate(error_test_cases): - exception, except_fn = get_error_details(test_case) - - pubnonces = [pnonce[i] for i in test_case["nonce_indices"]] - aggnonce = nonce_agg(pubnonces) - - pubkeys = [X[i] for i in test_case["key_indices"]] - tweaks = [tweak[i] for i in test_case["tweak_indices"]] - is_xonly = test_case["is_xonly"] - psigs = [psig[i] for i in test_case["psig_indices"]] - - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - assert_raises(exception, lambda: partial_sig_agg(psigs, session_ctx), except_fn) - -def test_sign_and_verify_random(iters: int) -> None: - for i in range(iters): - sk_1 = secrets.token_bytes(32) - sk_2 = secrets.token_bytes(32) - pk_1 = individual_pk(sk_1) - pk_2 = individual_pk(sk_2) - pubkeys = [pk_1, pk_2] - - # In this example, the message and aggregate pubkey are known - # before nonce generation, so they can be passed into the nonce - # generation function as a defense-in-depth measure to protect - # against nonce reuse. - # - # If these values are not known when nonce_gen is called, empty - # byte arrays can be passed in for the corresponding arguments - # instead. - msg = secrets.token_bytes(32) - v = secrets.randbelow(4) - tweaks = [secrets.token_bytes(32) for _ in range(v)] - is_xonly = [secrets.choice([False, True]) for _ in range(v)] - aggpk = get_xonly_pk(key_agg_and_tweak(pubkeys, tweaks, is_xonly)) - - # Use a non-repeating counter for extra_in - secnonce_1, pubnonce_1 = nonce_gen(sk_1, pk_1, aggpk, msg, i.to_bytes(4, 'big')) - - # On even iterations use regular signing algorithm for signer 2, - # otherwise use deterministic signing algorithm - if i % 2 == 0: - # Use a clock for extra_in - t = time.clock_gettime_ns(time.CLOCK_MONOTONIC) - secnonce_2, pubnonce_2 = nonce_gen(sk_2, pk_2, aggpk, msg, t.to_bytes(8, 'big')) - else: - aggothernonce = nonce_agg([pubnonce_1]) - rand = secrets.token_bytes(32) - pubnonce_2, psig_2 = deterministic_sign(sk_2, aggothernonce, pubkeys, tweaks, is_xonly, msg, rand) - - pubnonces = [pubnonce_1, pubnonce_2] - aggnonce = nonce_agg(pubnonces) - - session_ctx = SessionContext(aggnonce, pubkeys, tweaks, is_xonly, msg) - psig_1 = sign(secnonce_1, sk_1, session_ctx) - assert partial_sig_verify(psig_1, pubnonces, pubkeys, tweaks, is_xonly, msg, 0) - # An exception is thrown if secnonce_1 is accidentally reused - assert_raises(ValueError, lambda: sign(secnonce_1, sk_1, session_ctx), lambda e: True) - - # Wrong signer index - assert not partial_sig_verify(psig_1, pubnonces, pubkeys, tweaks, is_xonly, msg, 1) - - # Wrong message - assert not partial_sig_verify(psig_1, pubnonces, pubkeys, tweaks, is_xonly, secrets.token_bytes(32), 0) - - if i % 2 == 0: - psig_2 = sign(secnonce_2, sk_2, session_ctx) - assert partial_sig_verify(psig_2, pubnonces, pubkeys, tweaks, is_xonly, msg, 1) - - sig = partial_sig_agg([psig_1, psig_2], session_ctx) - assert schnorr_verify(msg, aggpk, sig) - -if __name__ == '__main__': - test_key_sort_vectors() - test_key_agg_vectors() - test_nonce_gen_vectors() - test_nonce_agg_vectors() - test_sign_verify_vectors() - test_tweak_vectors() - test_det_sign_vectors() - test_sig_agg_vectors() - test_sign_and_verify_random(6) diff --git a/crates/musig2/src/binary_encoding.rs b/crates/musig2/src/binary_encoding.rs deleted file mode 100644 index 47bb2bb0..00000000 --- a/crates/musig2/src/binary_encoding.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::errors::DecodeError; - -/// Marks a type which can be serialized to and from a binary encoding of either -/// fixed or variable length. -pub trait BinaryEncoding: Sized { - /// The binary type which is returned by serialization. Should either - /// be `[u8; N]` or `Vec`. - type Serialized; - - /// Serialize this data structure to its binary representation. - fn to_bytes(&self) -> Self::Serialized; - - /// Deserialize this data structure from a binary representation. - fn from_bytes(bytes: &[u8]) -> Result>; -} - -/// Implements various binary encoding traits for both fixed or -/// variable-length encoded data structures. -/// -/// Use this macro by first implementing [`BinaryEncoding`] on a type, -/// and then invoking `impl_encoding_traits` on the type. -macro_rules! impl_encoding_traits { - // Fixed length encoding - ($typename:ty, $byte_len:expr $(, $max_byte_len:expr)?) => { - /// assert that $typename implements `BinaryEncoding` - const _: () = { - fn __( - x: $typename, - ) -> impl BinaryEncoding - { - x - } - }; - - impl std::fmt::LowerHex for $typename { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let mut buffer = [0; $byte_len * 2]; - let encoded = base16ct::lower::encode_str(&self.to_bytes(), &mut buffer).unwrap(); - f.write_str(encoded) - } - } - - impl std::fmt::UpperHex for $typename { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let mut buffer = [0; $byte_len * 2]; - let encoded = base16ct::upper::encode_str(&self.to_bytes(), &mut buffer).unwrap(); - f.write_str(encoded) - } - } - - impl std::str::FromStr for $typename { - type Err = DecodeError; - - /// Parses this type from a hex string, which can be either upper or - /// lower case. The binary format of the decoded hex data should - /// match that returned by [`to_bytes`][Self::to_bytes]. - /// - /// Same as [`Self::from_hex`]. - fn from_str(hex: &str) -> Result { - let mut buffer = [0; $byte_len]; - let bytes = base16ct::mixed::decode(hex, &mut buffer)?; - Self::from_bytes(bytes) - } - } - - impl TryFrom<&[u8]> for $typename { - type Error = DecodeError; - - /// Parse this type from a variable-length byte slice. - /// - /// Same as [`Self::from_bytes`][Self::from_bytes]. - fn try_from(bytes: &[u8]) -> Result { - Self::from_bytes(bytes) - } - } - - impl TryFrom<[u8; $byte_len]> for $typename { - type Error = DecodeError; - - /// Parse this type from its fixed-length binary representation. - fn try_from(bytes: [u8; $byte_len]) -> Result { - Self::from_bytes(&bytes) - } - } - - impl TryFrom<&[u8; $byte_len]> for $typename { - type Error = DecodeError; - - /// Parse this type from its fixed-length binary representation. - /// - /// Same as [`Self::from_bytes`][Self::from_bytes]. - fn try_from(bytes: &[u8; $byte_len]) -> Result { - Self::from_bytes(bytes) - } - } - - $( - impl TryFrom<&[u8; $max_byte_len]> for $typename { - type Error = DecodeError; - - /// Parse this type from its maximum-length binary representation. - /// Throws away unused data. - /// - /// Same as [`Self::from_bytes`][Self::from_bytes]. - fn try_from(bytes: &[u8; $max_byte_len]) -> Result { - Self::from_bytes(bytes) - } - } - )? - - impl From<$typename> for [u8; $byte_len] { - /// Serialize this type to a fixed-length byte array. - fn from(value: $typename) -> Self { - value.to_bytes() - } - } - - impl From<$typename> for Vec { - /// Serialize this type to a heap-allocated byte vector. - fn from(value: $typename) -> Self { - Vec::from(value.to_bytes()) - } - } - - impl $typename { - /// Alias to [the `BinaryEncoding` trait implementation of `to_bytes`][Self::to_bytes]. - pub fn serialize(&self) -> [u8; $byte_len] { - ::to_bytes(self) - } - - /// Alias to [the `BinaryEncoding` trait implementation of `from_bytes`][Self::from_bytes]. - pub fn from_bytes(bytes: &[u8]) -> Result> { - ::from_bytes(bytes) - } - - /// Parses this type from a hex string, which can be either upper or - /// lower case. The binary format of the decoded hex data should - /// match that returned by [`to_bytes`][Self::to_bytes]. - /// - /// Same as [`Self::from_str`](#method.from_str). - pub fn from_hex(hex: &str) -> Result> { - hex.parse() - } - } - - #[cfg(any(test, feature = "serde"))] - impl serde::Serialize for $typename { - fn serialize(&self, serializer: S) -> Result { - let bytes = self.to_bytes(); - serdect::array::serialize_hex_lower_or_bin(&bytes, serializer) - } - } - - #[cfg(any(test, feature = "serde"))] - impl<'de> serde::Deserialize<'de> for $typename { - /// Deserializes this type from a byte array or a hex - /// string, depending on the human-readability of the data format. - fn deserialize>(deserializer: D) -> Result { - #[allow(unused_mut, unused_variables)] - let mut buffer = [0u8; $byte_len]; - - // Used for a type like SecNonce where we need to accept a longer encoding - // and throw away the unused bytes. - $(let mut buffer = [0u8; $max_byte_len];)? - - let bytes = serdect::slice::deserialize_hex_or_bin(&mut buffer, deserializer)?; - <$typename>::from_bytes(bytes).map_err(|_| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Bytes(&bytes), - &concat!("a byte array representing ", stringify!($typename)), - ) - }) - } - } - }; - - // Variable-length encoding - ($typename:ty) => { - /// assert that $typename implements `BinaryEncoding` - const _: () = { - fn __( - x: $typename, - ) -> impl BinaryEncoding> { - x - } - }; - - impl std::fmt::LowerHex for $typename { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let bytes = self.to_bytes(); - let mut buffer = vec![0; bytes.len() * 2]; - let encoded = base16ct::lower::encode_str(&bytes, &mut buffer).unwrap(); - f.write_str(encoded) - } - } - - impl std::fmt::UpperHex for $typename { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let bytes = self.to_bytes(); - let mut buffer = vec![0; bytes.len() * 2]; - let encoded = base16ct::upper::encode_str(&bytes, &mut buffer).unwrap(); - f.write_str(encoded) - } - } - - impl std::str::FromStr for $typename { - type Err = DecodeError; - - /// Parses this type from a hex string, which can be either upper or - /// lower case. The binary format of the decoded hex data should - /// match that returned by [`to_bytes`][Self::to_bytes]. - /// - /// Same as [`Self::from_hex`]. - fn from_str(hex: &str) -> Result { - let bytes = base16ct::mixed::decode_vec(hex)?; - Self::from_bytes(&bytes) - } - } - - impl TryFrom<&[u8]> for $typename { - type Error = DecodeError; - - /// Parse this type from a variable-length byte slice. - /// - /// Same as [`Self::from_bytes`][Self::from_bytes]. - fn try_from(bytes: &[u8]) -> Result { - Self::from_bytes(bytes) - } - } - - impl From<$typename> for Vec { - /// Serialize this type to a heap-allocated byte vector. - fn from(value: $typename) -> Self { - value.to_bytes() - } - } - - impl $typename { - /// Alias to [the `BinaryEncoding` trait implementation of `to_bytes`][Self::to_bytes]. - pub fn serialize(&self) -> Vec { - ::to_bytes(self) - } - - /// Alias to [the `BinaryEncoding` trait implementation of `from_bytes`][Self::from_bytes]. - pub fn from_bytes(bytes: &[u8]) -> Result> { - ::from_bytes(bytes) - } - - /// Parses this type from a hex string, which can be either upper or - /// lower case. The binary format of the decoded hex data should - /// match that returned by [`to_bytes`][Self::to_bytes]. - /// - /// Same as [`Self::from_str`](#method.from_str). - pub fn from_hex(hex: &str) -> Result> { - hex.parse() - } - } - - #[cfg(any(test, feature = "serde"))] - impl serde::Serialize for $typename { - fn serialize(&self, serializer: S) -> Result { - let bytes = self.to_bytes(); - serdect::slice::serialize_hex_lower_or_bin(&bytes, serializer) - } - } - - #[cfg(any(test, feature = "serde"))] - impl<'de> serde::Deserialize<'de> for $typename { - /// Deserializes this type from a byte vector or a hex - /// string, depending on the human-readability of the data format. - fn deserialize>(deserializer: D) -> Result { - let bytes = serdect::slice::deserialize_hex_or_bin_vec(deserializer)?; - <$typename>::from_bytes(&bytes).map_err(|_| { - serde::de::Error::invalid_value( - serde::de::Unexpected::Bytes(&bytes), - &concat!("a byte vector representing ", stringify!($typename)), - ) - }) - } - } - }; -} - -/// Implements the Display trait for a type by formatting it as a lower-case -/// hex string. -macro_rules! impl_hex_display { - ($typename:ident) => { - impl std::fmt::Display for $typename { - /// Formats this type as a lower-case hex string. - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{:x}", self) - } - } - }; -} diff --git a/crates/musig2/src/bip340.rs b/crates/musig2/src/bip340.rs deleted file mode 100644 index 23e31efa..00000000 --- a/crates/musig2/src/bip340.rs +++ /dev/null @@ -1,445 +0,0 @@ -use crate::errors::VerifyError; -use crate::{ - compute_challenge_hash_tweak, tagged_hashes, xor_bytes, AdaptorSignature, CompactSignature, - LiftedSignature, NonceSeed, -}; - -use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; - -#[cfg(any(test, feature = "rand"))] -use rand::SeedableRng as _; - -use sha2::Digest as _; -use subtle::ConstantTimeEq as _; - -/// Create a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) -/// Schnorr adaptor signature on the given message with a single private key. -/// -/// The resulting signature is verifiably encrypted under the given `adaptor_point`, -/// such that it can only be considered valid under BIP340 if it is then -/// _adapted_ using the discrete log of `adaptor_point`. See -/// [`AdaptorSignature::adapt`] to decrypt it once you know the adaptor secret. -/// -/// You can also compute the adaptor secret from the final decrypted signature, -/// if you can find it. -/// -/// This is provided in case MuSig implementations may wish to make use of -/// signatures to non-interactively prove the origin of a message. For example, -/// if all messages between co-signers are signed, then peers can assign blame -/// to any dishonest signers by sharing a copy of their dishonest message, which -/// will bear their signature. -pub fn sign_solo_adaptor( - seckey: impl Into, - message: impl AsRef<[u8]>, - nonce_seed: impl Into, - adaptor_point: impl Into, -) -> AdaptorSignature { - let seckey: Scalar = seckey.into(); - let nonce_seed: NonceSeed = nonce_seed.into(); - - let pubkey = seckey.base_point_mul(); - let d = seckey.negate_if(pubkey.parity()); - - let h: [u8; 32] = tagged_hashes::BIP0340_AUX_TAG_HASHER - .clone() - .chain_update(nonce_seed.0) - .finalize() - .into(); - - let t = xor_bytes(&h, &d.serialize()); - - let rand: [u8; 32] = tagged_hashes::BIP0340_NONCE_TAG_HASHER - .clone() - .chain_update(t) - .chain_update(pubkey.serialize_xonly()) - .chain_update(message.as_ref()) - .finalize() - .into(); - - // BIP340 says to fail if we get a nonce reducing to zero, but this is so - // unlikely that the failure condition is not worth it. Default to 1 instead. - let prenonce = match MaybeScalar::reduce_from(&rand) { - MaybeScalar::Zero => Scalar::one(), - MaybeScalar::Valid(k) => k, - }; - - let R = prenonce * G; // encrypted nonce - let adapted_nonce = R + adaptor_point.into(); - - // If the adapted nonce is odd-parity, we must negate our nonce and - // later also negate the adaptor secret at decryption time. - let k = prenonce.negate_if(adapted_nonce.parity()); - - let nonce_x_bytes = adapted_nonce.serialize_xonly(); - let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &pubkey, message); - - let s = k + e * d; - - AdaptorSignature::new(R, s) -} - -/// Create a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) -/// Schnorr signature on the given message with a single private key. -/// -/// This is provided in case MuSig implementations may wish to make use of -/// signatures to non-interactively prove the origin of a message. For example, -/// if all messages between co-signers are signed, then peers can assign blame -/// to any dishonest signers by sharing a copy of their dishonest message, which -/// will bear their signature. -/// -/// This function is effectively the same as [`sign_solo_adaptor`] but passing -/// [`MaybePoint::Infinity`] as the adaptor point. -pub fn sign_solo( - seckey: impl Into, - message: impl AsRef<[u8]>, - nonce_seed: impl Into, -) -> T -where - T: From, -{ - sign_solo_adaptor(seckey, message, nonce_seed, MaybePoint::Infinity) - .adapt(MaybeScalar::Zero) - .map(T::from) - .expect("signing with empty adaptor should never result in an adaptor failure") -} - -/// Verifies a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) -/// Schnorr adaptor signature, which could be aggregated or from a single-signer. -/// -/// The signature will verify only if it is encrypted under the given adaptor point. -/// -/// The `signature` argument is parsed as a [`LiftedSignature`]. You may pass any -/// type which converts fallibly to a [`LiftedSignature`], including `&[u8]`, `[u8; 64]`, -/// [`CompactSignature`], and so on. -/// -/// Returns an error if the adaptor signature is invalid, which includes -/// if the signature has been decrypted and is a fully valid signature. -pub fn verify_single_adaptor( - pubkey: impl Into, - adaptor_signature: &AdaptorSignature, - message: impl AsRef<[u8]>, - adaptor_point: impl Into, -) -> Result<(), VerifyError> { - use VerifyError::BadSignature; - - let pubkey: Point = pubkey.into().to_even_y(); // lift_x(x(P)) - - let &AdaptorSignature { R, s } = adaptor_signature; - - let adapted_nonce = R + adaptor_point.into(); - let nonce_x_bytes = adapted_nonce.serialize_xonly(); - let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &pubkey, message); - - // If the adapted nonce is odd-parity, the signer should have negated their nonce - // when signing. - let effective_nonce = if adapted_nonce.has_even_y() { R } else { -R }; - - // sG = R + eD - if s * G != effective_nonce + e * pubkey { - return Err(BadSignature); - } - - Ok(()) -} - -/// Verifies a [BIP340-compatible](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) -/// Schnorr signature, which could be aggregated or from a single-signer. -/// -/// The `signature` argument is parsed as a [`CompactSignature`]. You may pass any -/// type which converts fallibly to a [`CompactSignature`], including `&[u8]`, `[u8; 64]`, -/// [`LiftedSignature`], and so on. -/// -/// Returns an error if the signature is invalid. -pub fn verify_single( - pubkey: impl Into, - signature: impl TryInto, - message: impl AsRef<[u8]>, -) -> Result<(), VerifyError> { - use VerifyError::BadSignature; - - let pubkey: Point = pubkey.into().to_even_y(); // lift_x(x(P)) - let CompactSignature { rx, s } = signature.try_into().map_err(|_| BadSignature)?; - let e: MaybeScalar = compute_challenge_hash_tweak(&rx, &pubkey, message); - - // Instead of the usual sG = R + eD schnorr equation, we swap things around - // slightly, thus avoiding the need to lift the x-only nonce. - // - // sG = R + eD - // R = sG - eD - let verification_point = (s * G - e * pubkey).not_inf().map_err(|_| BadSignature)?; - if verification_point.has_odd_y() { - return Err(BadSignature); - } - - let valid = verification_point.serialize_xonly().ct_eq(&rx); - if bool::from(valid) { - Ok(()) - } else { - Err(BadSignature) - } -} - -/// Represents a pre-processed entry in a batch of signatures to be verified. -/// This can encapsulate either a normal BIP340 signature, or an adaptor signature. -/// -/// To verify a large number of signatures efficiently, pass a slice of -/// [`BatchVerificationRow`] to [`verify_batch`]. -#[cfg(any(test, feature = "rand"))] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct BatchVerificationRow { - pubkey: Point, - challenge: MaybeScalar, - R: MaybePoint, - s: MaybeScalar, -} - -#[cfg(any(test, feature = "rand"))] -impl BatchVerificationRow { - /// Construct a row in a batch verification table from a given BIP340 signature. - pub fn from_signature>( - pubkey: impl Into, - message: M, - signature: LiftedSignature, - ) -> Self { - let pubkey = pubkey.into(); - let challenge = - compute_challenge_hash_tweak(&signature.R.serialize_xonly(), &pubkey, message.as_ref()); - - BatchVerificationRow { - pubkey, - challenge, - R: MaybePoint::Valid(signature.R), - s: signature.s, - } - } - - /// Construct a row in a batch verification table from a given BIP340 adaptor signature. - pub fn from_adaptor_signature>( - pubkey: impl Into, - message: M, - adaptor_signature: AdaptorSignature, - adaptor_point: MaybePoint, - ) -> Self { - let pubkey = pubkey.into(); - let adapted_nonce = adaptor_signature.R + adaptor_point; - - // If the adapted nonce is odd-parity, the signer should have negated their nonce - // when signing. - let effective_nonce = if adapted_nonce.has_even_y() { - adaptor_signature.R - } else { - -adaptor_signature.R - }; - - let challenge = compute_challenge_hash_tweak( - &adapted_nonce.serialize_xonly(), - &pubkey, - message.as_ref(), - ); - - BatchVerificationRow { - pubkey, - challenge, - R: effective_nonce, - s: adaptor_signature.s, - } - } -} - -/// Runs [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) -/// batch verification on a collection of schnorr signatures. -/// -/// Batch verification checks a table of pubkeys, messages, and -/// signatures and returns an error if any signatures in the -/// collection are not valid for the corresponding `(pubkey, message)` -/// pair. -/// -/// Batch verification enables noteworthy speedups when verifying -/// large numbers of signatures, but does not give any indication -/// of _which_ signature(s) were invalid upon failure. Manual -/// investigation would be needed to narrow down which signature(s) -/// caused the verification to fail. -/// -/// This requires the `rand` library for access to a seedable CSPRNG. -/// The RNG is seeded with all the pubkeys, messages, and signatures -/// rather than being truly random. -#[cfg(any(test, feature = "rand"))] -pub fn verify_batch(rows: &[BatchVerificationRow]) -> Result<(), VerifyError> { - // Seed the CSPRNG - let mut rng = { - let mut seed_hash = tagged_hashes::BIP0340_BATCH_TAG_HASHER.clone(); - - // Challenges commit to the pubkey, nonce, and message. That's why - // we're not explicitly seeding the RNG with the pubkey, nonce, and message - // as suggested by BIP340. - for row in rows { - seed_hash.update(row.challenge.serialize()); - } - - for row in rows { - seed_hash.update(row.s.serialize()); - } - rand::rngs::StdRng::from_seed(seed_hash.finalize().into()) - }; - - let mut lhs = MaybeScalar::Zero; - let mut rhs_terms = Vec::::with_capacity(rows.len() * 2); - - for (i, row) in rows.iter().enumerate() { - let random = if i == 0 { - Scalar::one() - } else { - Scalar::random(&mut rng) - }; - - let pubkey = row.pubkey.to_even_y(); // lift_x on all pubkeys - - lhs += row.s * random; - rhs_terms.push(row.R * random); - rhs_terms.push((random * row.challenge) * pubkey); - } - - // (s1*a1 + s2*a2 + ... + sn*an)G ?= (a1*R1) + (a2*R2) + ... + (an*Rn) + - // (a1*e1*P1) + (a2*e2*P2) + ... + (an*en*Pn) - let rhs = MaybePoint::sum(rhs_terms); - if lhs * G == rhs { - Ok(()) - } else { - Err(VerifyError::BadSignature) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{testhex, BinaryEncoding, CompactSignature}; - use secp::Scalar; - - #[test] - fn test_bip340_signatures() { - const BIP340_TEST_VECTORS: &[u8] = include_bytes!("test_vectors/bip340_vectors.csv"); - - #[derive(serde::Deserialize)] - struct TestVectorRecord { - index: usize, - #[serde(rename = "secret key")] - seckey: Option, - #[serde(rename = "public key", deserialize_with = "testhex::deserialize")] - pubkey_x: [u8; 32], - #[serde(deserialize_with = "testhex::deserialize")] - aux_rand: Vec, - #[serde(deserialize_with = "testhex::deserialize")] - message: Vec, - signature: String, - #[serde(rename = "verification result")] - verification_result: String, - comment: String, - } - - let mut csv_reader = csv::Reader::from_reader(BIP340_TEST_VECTORS); - - let mut valid_sigs_batch = Vec::::new(); - - for result in csv_reader.deserialize() { - let record: TestVectorRecord = result.expect("failed to parse BIP340 test vector"); - - let pubkey = match Point::lift_x(&record.pubkey_x) { - Ok(p) => p, - Err(_) => { - if record.verification_result == "TRUE" { - panic!( - "expected verification to succeed on invalid public key {}", - base16ct::lower::encode_string(&record.pubkey_x) - ); - } - continue; // not a test case we have to worry about. - } - }; - - let test_vec_signature: [u8; 64] = base16ct::mixed::decode_vec(&record.signature) - .unwrap_or_else(|_| panic!("invalid signature hex: {}", record.signature)) - .try_into() - .expect("invalid signature length"); - - if let Some(seckey) = record.seckey { - let aux_rand = - <[u8; 32]>::try_from(record.aux_rand.as_slice()).unwrap_or_else(|_| { - panic!( - "invalid aux_rand: {}", - base16ct::lower::encode_string(&record.aux_rand) - ) - }); - - let created_signature: CompactSignature = - sign_solo(seckey, &record.message, aux_rand); - - assert_eq!( - created_signature.to_bytes(), - test_vec_signature, - "test vector signature does not match for test vector {}; {}", - record.index, - &record.comment - ); - - // Test adaptor signatures - { - let adaptor_secret = MaybeScalar::Valid(seckey); // arbitrary secret - let adaptor_point = adaptor_secret * G; - let adaptor_signature = - sign_solo_adaptor(seckey, &record.message, aux_rand, adaptor_point); - - verify_single_adaptor( - pubkey, - &adaptor_signature, - &record.message, - adaptor_point, - ) - .expect("failed to verify valid adaptor signature"); - - // Ensure the decrypted signature is valid. - let valid_sig = adaptor_signature.adapt(adaptor_secret).unwrap(); - verify_single(pubkey, valid_sig, &record.message) - .expect("failed to verify decrypted adaptor signature"); - - // Ensure observers can learn the adaptor secret from published signatures. - let revealed: MaybeScalar = adaptor_signature - .reveal_secret(&valid_sig) - .expect("decrypted signature should reveal adaptor secret"); - assert_eq!(revealed, adaptor_secret); - } - } - - let verify_result = verify_single(pubkey, test_vec_signature, &record.message); - match record.verification_result.as_str() { - "TRUE" => { - verify_result.unwrap_or_else(|_| { - panic!( - "verification should pass for signature {} - {}", - &record.signature, record.comment - ) - }); - valid_sigs_batch.push(BatchVerificationRow::from_signature( - pubkey, - record.message, - LiftedSignature::try_from(test_vec_signature).unwrap(), - )); - } - - "FALSE" => { - assert_eq!( - verify_result, - Err(VerifyError::BadSignature), - "verification should fail for signature {} - {}", - &record.signature, - record.comment - ); - } - - s => panic!("unexpected verification result column value: {}", s), - }; - } - - // test batch verification - verify_batch(&valid_sigs_batch).expect("batch verification failed"); - } -} diff --git a/crates/musig2/src/deterministic.rs b/crates/musig2/src/deterministic.rs deleted file mode 100644 index a70e49f1..00000000 --- a/crates/musig2/src/deterministic.rs +++ /dev/null @@ -1,147 +0,0 @@ -//! This module provides determinstic BIP340-compatible single-signer logic using -//! [RFC6979](https://www.rfc-editor.org/rfc/rfc6979). -//! -//! This approach produces a synthetic nonce by deriving it from a -//! chained hash of the private key and and the message to be signed. -//! Generating nonces in this way makes signatatures deterministic. -//! -//! Technically RFC6979 is not part of the BIP340 spec, but it is entirely valid -//! to use deterministic nonce generation, provided you can guarantee that the -//! `(seckey, message)` pair are never used for other deterministic signatures -//! outside of BIP340. -//! -//! This is safe in a single-signer environment only (not for MuSig). -//! For deterministic nonces in a multi-signer environment, you will need -//! zero-knowledge proofs. See [this paper for details](https://eprint.iacr.org/2020/1057.pdf). -use secp::{MaybePoint, Scalar}; - -use crate::{AdaptorSignature, LiftedSignature}; - -use hmac::digest::FixedOutput as _; -use hmac::Mac as _; -use sha2::Digest as _; - -fn hmac_sha256(key: &[u8; 32], msg: &[u8]) -> [u8; 32] { - hmac::Hmac::::new_from_slice(key.as_ref()) - .expect("Hmac::new_from_slice never fails") - .chain_update(msg) - .finalize_fixed() - .into() -} - -/// Derive a nonce from a given `(seckey, message)` pair. Follows the procedure -/// laid out in [this section of the RFC](https://www.rfc-editor.org/rfc/rfc6979#section-3.2). -pub fn derive_nonce_rfc6979(seckey: impl Into, message: impl AsRef<[u8]>) -> Scalar { - let seckey = seckey.into(); - - let h1 = sha2::Sha256::new() - .chain_update(message.as_ref()) - .finalize(); - - let mut V = [1u8; 32]; - let mut K = [0u8; 32]; - - // Step D: - // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1)) - let mut buf = vec![0u8; 32 + 1 + 32 + 32]; - buf[..32].copy_from_slice(&V); - buf[32] = 0; - buf[33..65].copy_from_slice(&seckey.serialize()); - buf[65..].copy_from_slice(&h1); - K = hmac_sha256(&K, &buf); - - // Step E: - // V = HMAC_K(V) - V = hmac_sha256(&K, &V); - - // Step F: - // K = HMAC_K(V || 0x01 || int2octets(x) || bits2octets(h1)) - buf[..32].copy_from_slice(&V); - buf[32] = 1; - K = hmac_sha256(&K, &buf); - - // Step G: - // V = HMAC_K(V) - V = hmac_sha256(&K, &V); - - loop { - // Step H2: - // V = HMAC_K(V) - V = hmac_sha256(&K, &V); - - // Step H3: - // k = bits2int(V) - if let Ok(k) = Scalar::from_slice(&V) { - return k; - } - - buf[..32].copy_from_slice(&V); - buf[32] = 0; - K = hmac_sha256(&K, &buf[..33]); - V = hmac_sha256(&K, &V); - } -} - -/// This module provides a determinstic flavor of adaptor signature creation for single-signer contexts. -pub mod adaptor { - use super::*; - - /// This is the same as [`adaptor::sign_solo`][crate::adaptor::sign_solo] except using - /// deterministic nonce generation. - pub fn sign_solo( - seckey: impl Into, - message: impl AsRef<[u8]>, - adaptor_point: impl Into, - ) -> AdaptorSignature { - let seckey = seckey.into(); - let aux = derive_nonce_rfc6979(seckey, &message).serialize(); - crate::adaptor::sign_solo(seckey, message, aux, adaptor_point) - } -} - -/// This is the same as [`sign_solo`][crate::sign_solo] except using deterministic nonce generation. -pub fn sign_solo(seckey: impl Into, message: impl AsRef<[u8]>) -> T -where - T: From, -{ - let seckey = seckey.into(); - let aux = derive_nonce_rfc6979(seckey, &message).serialize(); - crate::sign_solo(seckey, message, aux) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rfc6979_nonces() { - struct TestVector { - seckey: Scalar, - message: &'static str, - expected_nonce: &'static str, - } - - let test_vectors = [ - // from https://www.rfc-editor.org/rfc/rfc6979#appendix-A.2.5 - TestVector { - seckey: "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721" - .parse() - .unwrap(), - message: "sample", - expected_nonce: "A6E3C57DD01ABE90086538398355DD4C3B17AA873382B0F24D6129493D8AAD60", - }, - TestVector { - seckey: "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721" - .parse() - .unwrap(), - message: "test", - expected_nonce: "D16B6AE827F17175E040871A1C7EC3500192C4C92677336EC2537ACAEE0008E0", - }, - ]; - - for test in test_vectors { - let nonce = derive_nonce_rfc6979(test.seckey, test.message); - assert_eq!(format!("{:X}", nonce), test.expected_nonce); - } - } -} diff --git a/crates/musig2/src/errors.rs b/crates/musig2/src/errors.rs deleted file mode 100644 index d30ab8b7..00000000 --- a/crates/musig2/src/errors.rs +++ /dev/null @@ -1,386 +0,0 @@ -//! Various error types for different kinds of failures. - -use crate::KeyAggContext; - -use std::error::Error; -use std::fmt; - -/// Returned when aggregating a collection of public keys with [`KeyAggContext`] -/// results in the point at infinity. -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct KeyAggError; -impl fmt::Display for KeyAggError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("computed an invalid aggregated key from a collection of public keys") - } -} -impl Error for KeyAggError {} -impl From for KeyAggError { - fn from(_: secp::errors::InfinityPointError) -> Self { - KeyAggError - } -} - -/// Returned when aggregating a collection of secret keys with [`KeyAggContext`], -/// but some secret keys are missing, or the keys are not the correct secret keys -/// for the pubkeys contained in the key agg context. -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct InvalidSecretKeysError; -impl fmt::Display for InvalidSecretKeysError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("missing or invalid secret keys provided for aggregation") - } -} -impl Error for InvalidSecretKeysError {} -impl From for InvalidSecretKeysError { - fn from(_: secp::errors::ZeroScalarError) -> Self { - InvalidSecretKeysError - } -} - -/// Returned when tweaking a [`KeyAggContext`] results in the point -/// at infinity, or if using [`KeyAggContext::with_taproot_tweak`] -/// when the tweak input results in a hash which exceeds the curve -/// order (exceedingly unlikely)" -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct TweakError; -impl fmt::Display for TweakError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str("tweak value is invalid") - } -} -impl Error for TweakError {} -impl From for TweakError { - fn from(_: secp::errors::InfinityPointError) -> Self { - TweakError - } -} - -/// Returned when passing a signer index which is out of range for a -/// group of signers -#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] -pub struct SignerIndexError { - /// The index of the signer we did not expect to receive. - pub index: usize, - - /// The total size of the signing group. - pub n_signers: usize, -} -impl fmt::Display for SignerIndexError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "signer index {} is out of range for group of {} signers", - self.index, self.n_signers - ) - } -} -impl Error for SignerIndexError {} - -impl SignerIndexError { - /// Construct a new `SignerIndexError` indicating we received an - /// invalid index for the given group size of signers. - pub(crate) fn new(index: usize, n_signers: usize) -> SignerIndexError { - SignerIndexError { index, n_signers } - } -} - -/// Error returned when (partial) signing fails. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum SigningError { - /// Indicates an unknown secret key was provided when - /// using [`sign_partial`][crate::sign_partial] or - /// finalizing the [`FirstRound`][crate::FirstRound]. - UnknownKey, - - /// We could not verify the signature we produced. - /// This may indicate a malicious actor attempted to make us - /// produce a signature which could reveal our secret key. The - /// signing session should be aborted and retried with new nonces. - SelfVerifyFail, -} -impl fmt::Display for SigningError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "failed to create signature: {}", - match self { - Self::UnknownKey => "signing key is not a member of the group", - Self::SelfVerifyFail => "failed to verify our own signature; something is wrong", - } - ) - } -} -impl Error for SigningError {} - -/// Error returned when verification fails. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum VerifyError { - /// Indicates a public key was provided which is not - /// a member of the signing group, and thus partial - /// signature verification on this key has no meaning. - UnknownKey, - - /// The signature is not valid for the given key and message. - BadSignature, -} -impl fmt::Display for VerifyError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "failed to verify signature: {}", - match self { - Self::UnknownKey => "public key is not a member of the group", - Self::BadSignature => "signature is invalid", - } - ) - } -} -impl Error for VerifyError {} - -impl From for SigningError { - fn from(_: VerifyError) -> Self { - SigningError::SelfVerifyFail - } -} - -/// Enumerates the causes for why receiving a contribution from a peer -/// might fail. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum ContributionFaultReason { - /// The signer's index is out of range for the given - /// number of signers in the group. Embeds `n_signers` - /// (the number of signers). - OutOfRange(usize), - - /// Indicates we received different contribution values from - /// this peer for the same round. If we receive the same - /// nonce or signature from this peer more than once this is - /// acceptable and treated as a no-op, but receiving inconsistent - /// contributions from the same signer may indicate there is - /// malicious behavior occurring. - InconsistentContribution, - - /// Indicates we received an invalid partial signature. Only returned by - /// [`SecondRound::receive_signature`][crate::SecondRound::receive_signature]. - InvalidSignature, -} - -/// This error is returned by when a peer provides an invalid contribution -/// to one of the signing rounds. -/// -/// This is either because the signer's index exceeds the maximum, or -/// because we received an invalid contribution from this signer. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct RoundContributionError { - /// The erroneous signer index. - pub index: usize, - - /// The reason why the signer's contribution was rejected. - pub reason: ContributionFaultReason, -} - -impl RoundContributionError { - /// Create a new out of range signer index error. - pub fn out_of_range(index: usize, n_signers: usize) -> RoundContributionError { - RoundContributionError { - index, - reason: ContributionFaultReason::OutOfRange(n_signers), - } - } - - /// Create an error caused by an inconsistent contribution. - pub fn inconsistent_contribution(index: usize) -> RoundContributionError { - RoundContributionError { - index, - reason: ContributionFaultReason::InconsistentContribution, - } - } - - /// Create a new error caused by an invalid partial signature. - pub fn invalid_signature(index: usize) -> RoundContributionError { - RoundContributionError { - index, - reason: ContributionFaultReason::InvalidSignature, - } - } -} - -impl fmt::Display for RoundContributionError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use ContributionFaultReason::*; - write!( - f, - "invalid signer index {}: {}", - self.index, - match self.reason { - OutOfRange(n_signers) => format!("exceeds max index for {} signers", n_signers), - InconsistentContribution => - "received inconsistent contributions from same signer".to_string(), - InvalidSignature => "received invalid partial signature from peer".to_string(), - } - ) - } -} - -impl Error for RoundContributionError {} - -/// Returned when finalizing [`FirstRound`][crate::FirstRound] or -/// [`SecondRound`][crate::SecondRound] fails. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum RoundFinalizeError { - /// Contributions from all signers in the group are required to finalize - /// a signing round. This error is returned if attempting to finalize - /// a round before all contributions are received. - Incomplete, - - /// Indicates partial signing failed unexpectedly. This is likely because - /// the wrong secret key was provided. Only returned by - /// [`FirstRound::finalize`][crate::FirstRound::finalize]. - SigningError(SigningError), - - /// Indicates the final aggregated signature is invalid. Only returned by - /// [`SecondRound::finalize`][crate::SecondRound::finalize]. - InvalidAggregatedSignature(VerifyError), -} - -impl fmt::Display for RoundFinalizeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "cannot finalize round: {}", - match self { - Self::Incomplete => "not all signers have contributed".to_string(), - Self::SigningError(e) => format!("signing failed, {}", e), - Self::InvalidAggregatedSignature(e) => - format!("could not verify aggregated signature: {}", e), - } - ) - } -} - -impl Error for RoundFinalizeError {} - -impl From for RoundFinalizeError { - fn from(e: SigningError) -> Self { - RoundFinalizeError::SigningError(e) - } -} - -impl From for RoundFinalizeError { - fn from(e: VerifyError) -> Self { - RoundFinalizeError::InvalidAggregatedSignature(e) - } -} - -/// Enumerates the various reasons why binary or hex decoding -/// could fail. -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum DecodeFailureReason { - /// The hex string's format was incorrect, which could mean - /// it either was the wrong length or held invalid characters. - BadHexFormat(base16ct::Error), - - /// The byte slice we tried to deserialize had the wrong length. - BadLength(usize), - - /// The bytes contained coordinates to a point that is not on - /// the secp256k1 curve. - InvalidPoint, - - /// The bytes slice contained a representation of a scalar which - /// is outside the required finite field's range. - InvalidScalar, - - /// Custom error reason. - Custom(String), -} - -/// Returned when decoding a certain data structure of type `T` fails. -/// -/// The type `T` only serves as a compile-time safety check; no -/// data of type `T` is actually owned by this error. -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct DecodeError { - /// The reason for the decoding failure. - pub reason: DecodeFailureReason, - phantom: std::marker::PhantomData, -} - -impl DecodeError { - /// Construct a new decoding error for type `T` given a cause - /// for the failure. - pub fn new(reason: DecodeFailureReason) -> Self { - DecodeError { - reason, - phantom: std::marker::PhantomData, - } - } - - /// Create a decoding error caused by an incorrect input byte - /// slice length. - pub fn bad_length(size: usize) -> Self { - let reason = DecodeFailureReason::BadLength(size); - DecodeError::new(reason) - } - - /// Create a custom decoding failure. - pub fn custom(s: impl fmt::Display) -> Self { - let reason = DecodeFailureReason::Custom(s.to_string()); - DecodeError::new(reason) - } - - /// Converts the decoding error for one type into that of another type. - pub fn convert(self) -> DecodeError { - DecodeError::new(self.reason) - } -} - -impl fmt::Display for DecodeError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use DecodeFailureReason::*; - - write!( - f, - "error decoding {}: {}", - std::any::type_name::(), - match &self.reason { - BadHexFormat(e) => format!("hex decoding error: {}", e), - BadLength(size) => format!("unexpected length {}", size), - InvalidPoint => secp::errors::InvalidPointBytes.to_string(), - InvalidScalar => secp::errors::InvalidScalarBytes.to_string(), - Custom(s) => s.to_string(), - } - ) - } -} - -impl From for DecodeError { - fn from(_: secp::errors::InvalidPointBytes) -> Self { - DecodeError::new(DecodeFailureReason::InvalidPoint) - } -} - -impl From for DecodeError { - fn from(_: secp::errors::InvalidScalarBytes) -> Self { - DecodeError::new(DecodeFailureReason::InvalidScalar) - } -} - -impl From for DecodeError { - fn from(e: base16ct::Error) -> Self { - DecodeError::new(DecodeFailureReason::BadHexFormat(e)) - } -} - -impl From for DecodeError { - fn from(e: KeyAggError) -> Self { - DecodeError::custom(e) - } -} - -impl From for DecodeError { - fn from(_: TweakError) -> Self { - DecodeError::custom("serialized KeyAggContext contains an invalid tweak") - } -} diff --git a/crates/musig2/src/key_agg.rs b/crates/musig2/src/key_agg.rs deleted file mode 100644 index a112621b..00000000 --- a/crates/musig2/src/key_agg.rs +++ /dev/null @@ -1,988 +0,0 @@ -use std::collections::HashMap; - -use rkyv::{ - with::{Identity, Map, MapKV}, - Archive, Deserialize, Serialize, -}; -use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; -use sha2::Digest as _; -use subtle::ConstantTimeEq as _; - -use crate::{ - errors::{DecodeError, InvalidSecretKeysError, KeyAggError, TweakError}, - rkyv_wrappers, tagged_hashes, BinaryEncoding, -}; - -/// Represents an aggregated and tweaked public key. -/// -/// A set of pubkeys can be aggregated into a `KeyAggContext` which -/// allows co-signers to cooperatively sign data. -/// -/// `KeyAggContext` is essentially a sequence of pubkeys and tweaks -/// which determine a final aggregated key, with which the whole -/// cohort can cooperatively sign messages. -/// -/// See [`KeyAggContext::with_tweak`] to learn -/// more about tweaking. -#[derive(Debug, Clone, Archive, Serialize, Deserialize)] -pub struct KeyAggContext { - /// The aggregated pubkey point `Q`. - #[rkyv(with = rkyv_wrappers::Point)] - pub(crate) pubkey: Point, - - /// The component individual pubkeys in their original order. - #[rkyv(with = Map)] - pub(crate) ordered_pubkeys: Vec, - - /// A map of pubkeys to their indexes in the [`ordered_pubkeys`][Self::ordered_pubkeys] - /// field. - #[rkyv(with = MapKV)] - pub(crate) pubkey_indexes: HashMap, - - /// Cached key aggregation coefficients of individual pubkeys, in the - /// same order as `ordered_pubkeys`. - #[rkyv(with = Map)] - pub(crate) key_coefficients: Vec, - - /// A cache of effective individual pubkeys, i.e. `pubkey * self.key_coefficient(pubkey)`. - #[rkyv(with = Map)] - pub(crate) effective_pubkeys: Vec, - - #[rkyv(with = rkyv_wrappers::Choice)] - pub(crate) parity_acc: subtle::Choice, // false means g=1, true means g=n-1 - #[rkyv(with = rkyv_wrappers::MaybeScalar)] - pub(crate) tweak_acc: MaybeScalar, // None means zero. -} - -impl KeyAggContext { - /// Constructs a key aggregation context for a given set of pubkeys. - /// The order in which the pubkeys are presented by the iterator will be preserved. - /// A specific ordering of pubkeys will uniquely determine the aggregated public key. - /// - /// If the same keys are provided again in a different sorting order, a different - /// aggregated pubkey will result. We recommended to sort keys ahead of time - /// in some deterministic fashion before constructing a `KeyAggContext`. - /// - /// ``` - #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::PublicKey;")] - #[cfg_attr( - all(feature = "k256", not(feature = "secp256k1")), - doc = "use secp::Point as PublicKey;" - )] - /// use musig2::KeyAggContext; - /// - /// let mut pubkeys: [PublicKey; 3] = [ - /// "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" - /// .parse() - /// .unwrap(), - /// "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" - /// .parse() - /// .unwrap(), - /// "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" - /// .parse() - /// .unwrap(), - /// ]; - /// - /// let key_agg_ctx = KeyAggContext::new(pubkeys) - /// .expect("error aggregating pubkeys"); - /// - /// pubkeys.sort(); - /// let sorted_key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); - /// - /// let pk: PublicKey = key_agg_ctx.aggregated_pubkey(); - /// let pk_sorted: PublicKey = sorted_key_agg_ctx.aggregated_pubkey(); - /// assert_ne!(pk, pk_sorted); - /// ``` - /// - /// Multiple copies of the same public key are also accepted. They will - /// be aggregated together and all signers will be expected to provide - /// valid signatures from their key. - /// - /// Signers will be identified by their index from zero. The first key - /// returned from the `pubkeys` iterator will be signer `0`. The second - /// key will be index `1`, and so on. It is important that the caller can - /// clearly identify every signer, so that they know who to blame if - /// a signing contribution (e.g. a partial signature) is invalid. - pub fn new(pubkeys: I) -> Result - where - I: IntoIterator, - P: Into, - { - let ordered_pubkeys: Vec = pubkeys.into_iter().map(|p| p.into()).collect(); - assert!(!ordered_pubkeys.is_empty(), "received empty set of pubkeys"); - assert!( - ordered_pubkeys.len() <= u32::MAX as usize, - "max number of pubkeys is u32::MAX" - ); - - // If all pubkeys are the same, `pk2` will be set to `None`, indicating - // that every public key `X` should be tweaked with a coefficient `H_agg(L, X)` - // to prevent collisions (See appendix B of the musig2 paper). - let pk2: Option<&Point> = ordered_pubkeys[1..] - .iter() - .find(|pubkey| pubkey != &&ordered_pubkeys[0]); - - let pk_list_hash = hash_pubkeys(&ordered_pubkeys); - - let (effective_pubkeys, key_coefficients): (Vec, Vec) = - ordered_pubkeys - .iter() - .map(|&pubkey| { - let key_coeff = - compute_key_aggregation_coefficient(&pk_list_hash, &pubkey, pk2); - (pubkey * key_coeff, key_coeff) - }) - .unzip(); - - let aggregated_pubkey = MaybePoint::sum(&effective_pubkeys).not_inf()?; - - let pubkey_indexes = HashMap::from_iter( - ordered_pubkeys - .iter() - .copied() - .enumerate() - .map(|(i, pk)| (pk, i)), - ); - - Ok(KeyAggContext { - pubkey: aggregated_pubkey, - ordered_pubkeys, - pubkey_indexes, - key_coefficients, - effective_pubkeys, - parity_acc: subtle::Choice::from(0), - tweak_acc: MaybeScalar::Zero, - }) - } - - /// Tweak the key aggregation context with a specific scalar tweak value. - /// - /// 'Tweaking' is the practice of committing a key to an agreed-upon scalar - /// value, such as a SHA256 hash. In Bitcoin contexts, this is used for - /// [taproot](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) - /// script commitments, or - /// [BIP32 key derivation](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki). - /// - /// Signatures created using the resulting tweaked key aggregation context will be - /// bound to this tweak value. - /// - /// A verifier can later prove that the signer(s) committed to this value - /// if the `tweak` value was itself generated by committing to the public key, - /// e.g. by hashing the aggregated public key. - /// - /// The `is_xonly` argument determines whether the tweak should be applied to - /// the plain aggregated pubkey, or to the even-parity (i.e. x-only) aggregated - /// pubkey. `is_xonly` should be true for applying Bitcoin taproot commitments, - /// and false for applying BIP32 key derivation tweaks. - /// - /// Returns an error if the tweaked public key would be the point at infinity. - /// - /// ``` - #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::{PublicKey, SecretKey};")] - #[cfg_attr( - all(feature = "k256", not(feature = "secp256k1")), - doc = "use secp::{Point as PublicKey, Scalar as SecretKey};" - )] - /// use musig2::KeyAggContext; - /// - /// let pubkeys = [ - /// "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" - /// .parse::() - /// .unwrap(), - /// "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" - /// .parse::() - /// .unwrap(), - /// ]; - /// - /// let key_agg_ctx = KeyAggContext::new(pubkeys) - /// .unwrap() - /// .with_tweak( - /// "7931676703c0865d8b502dcdf1d956e86503796cfeabe33d12a918fbf408da05" - /// .parse::() - /// .unwrap(), - /// false - /// ) - /// .unwrap(); - /// - /// let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey_untweaked(); - /// - /// assert_eq!( - /// aggregated_pubkey.to_string(), - /// "0385eb6101982e142dba553cae437d08a82880fe9a22889c997f8e415a61b7a2d5" - /// ); - pub fn with_tweak(self, tweak: impl Into, is_xonly: bool) -> Result { - if is_xonly { - self.with_xonly_tweak(tweak) - } else { - self.with_plain_tweak(tweak) - } - } - - /// Iteratively applies tweaks to the aggregated pubkey. See [`KeyAggContext::with_tweak`]. - pub fn with_tweaks(mut self, tweaks: I) -> Result - where - I: IntoIterator, - S: Into, - { - for (tweak, is_xonly) in tweaks.into_iter() { - self = self.with_tweak(tweak, is_xonly)?; - } - Ok(self) - } - - /// Same as `self.with_tweak(tweak, false)`. See [`KeyAggContext::with_tweak`]. - pub fn with_plain_tweak(self, tweak: impl Into) -> Result { - let tweak: Scalar = tweak.into(); - - // Q' = Q + t*G - let tweaked_pubkey = (self.pubkey + (tweak * G)).not_inf()?; - - // tacc' = t + tacc - let new_tweak_acc = self.tweak_acc + tweak; - - Ok(KeyAggContext { - pubkey: tweaked_pubkey, - tweak_acc: new_tweak_acc, - ..self - }) - } - - /// Same as `self.with_tweak(tweak, true)`. See [`KeyAggContext::with_tweak`]. - pub fn with_xonly_tweak(self, tweak: impl Into) -> Result { - // if has_even_y(Q): g = 1 (Same as a plain tweak.) - // else: g = n - 1 - if self.pubkey.has_even_y() { - return self.with_plain_tweak(tweak); - } - - let tweak: Scalar = tweak.into(); - - // Q' = g*Q + t*G - // - // Negating the pubkey point Q is the same as multiplying it - // by (n-1), but is much faster. - let tweaked_pubkey = (tweak * G - self.pubkey).not_inf()?; - - // tacc' = g*tacc + t - // - // Negating the tweak accumulator is the same as multiplying it - // by (n-1), but is much faster. - let new_tweak_acc = tweak - self.tweak_acc; - - Ok(KeyAggContext { - pubkey: tweaked_pubkey, - parity_acc: !self.parity_acc, - tweak_acc: new_tweak_acc, - ..self - }) - } - - fn with_taproot_tweak_internal(self, merkle_root: &[u8]) -> Result { - // t = int(H_taptweak(xbytes(P), k)) - let tweak_hash: [u8; 32] = tagged_hashes::TAPROOT_TWEAK_TAG_HASHER - .clone() - .chain_update(self.pubkey.serialize_xonly()) - .chain_update(merkle_root) - .finalize() - .into(); - - let tweak = Scalar::try_from(tweak_hash).map_err(|_| TweakError)?; - self.with_xonly_tweak(tweak) - } - - /// Tweak the key aggregation context with the given tapscript merkle tree root hash. - /// - /// This is used to commit the key aggregation context to a specific tree of Bitcoin - /// taproot scripts, determined by the given `merkle_root` hash. Computing the merkle - /// tree root is outside the scope of this package. See - /// [BIP341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) - /// for details of how tapscript merkle trees are constructed. - /// - /// The tweak value `t` is computed as: - /// - /// ```notrust - /// prefix = sha256(b"TapTweak") - /// tweak_hash = sha256( - /// prefix, - /// prefix, - /// self.aggregated_pubkey().serialize_xonly(), - /// merkle_root - /// ) - /// t = int(tweak_hash) - /// ``` - /// - /// Note that the _current tweaked aggregated pubkey_ is hashed, not - /// the plain untweaked pubkey. - pub fn with_taproot_tweak(self, merkle_root: &[u8; 32]) -> Result { - self.with_taproot_tweak_internal(merkle_root.as_ref()) - } - - /// Tweak the key aggregation context with an empty unspendable merkle root. - /// - /// This allows a 3rd party observer (who doesn't know the constituent musig group - /// member keys) to verify, given the untweaked group key, that the tweaked group - /// key does not commit to any hidden tapscript trees. See [BIP341 for more info][BIP341]. - /// - /// The tweak value `t` is computed as: - /// - /// ```notrust - /// prefix = sha256(b"TapTweak") - /// tweak_hash = sha256( - /// prefix, - /// prefix, - /// self.aggregated_pubkey().serialize_xonly(), - /// ) - /// t = int(tweak_hash) - /// ``` - /// - /// Note that the _current tweaked aggregated pubkey_ is hashed, not - /// the plain untweaked pubkey. - /// - /// [BIP341]: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#cite_note-23 - pub fn with_unspendable_taproot_tweak(self) -> Result { - self.with_taproot_tweak_internal(b"") - } - - /// Returns the aggregated public key, converted to a given type. - /// - /// ``` - #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::PublicKey;")] - #[cfg_attr( - all(feature = "k256", not(feature = "secp256k1")), - doc = "use secp::Point as PublicKey;" - )] - /// use musig2::KeyAggContext; - /// - /// let pubkeys: Vec = vec![ - /// /* ... */ - /// # "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" - /// # .parse() - /// # .unwrap(), - /// # "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" - /// # .parse() - /// # .unwrap(), - /// # "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" - /// # .parse() - /// # .unwrap(), - /// ]; - /// - /// let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); - /// let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey(); - /// assert_eq!( - /// aggregated_pubkey.to_string(), - /// "0290539eede565f5d054f32cc0c220126889ed1e5d193baf15aef344fe59d4610c" - /// ) - /// ``` - /// - /// If any tweaks have been applied to the `KeyAggContext`, the the pubkey - /// returned by this method will be the tweaked aggregate public key, and - /// not the plain aggregated key. - pub fn aggregated_pubkey>(&self) -> T { - T::from(self.pubkey) - } - - /// Returns the aggregated pubkey without any tweaks. - /// - /// ``` - #[cfg_attr(feature = "secp256k1", doc = "use secp256k1::{PublicKey, SecretKey};")] - #[cfg_attr( - all(feature = "k256", not(feature = "secp256k1")), - doc = "use secp::{Point as PublicKey, Scalar as SecretKey};" - )] - /// use musig2::KeyAggContext; - /// - /// let pubkeys = [ - /// /* ... */ - /// # "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" - /// # .parse::() - /// # .unwrap(), - /// # "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66" - /// # .parse::() - /// # .unwrap(), - /// ]; - /// - /// let key_agg_ctx = KeyAggContext::new(pubkeys) - /// .unwrap() - /// .with_xonly_tweak( - /// "7931676703c0865d8b502dcdf1d956e86503796cfeabe33d12a918fbf408da05" - /// .parse::() - /// .unwrap() - /// ) - /// .unwrap(); - /// - /// let aggregated_pubkey: PublicKey = key_agg_ctx.aggregated_pubkey_untweaked(); - /// - /// assert_eq!( - /// aggregated_pubkey, - /// KeyAggContext::new(pubkeys).unwrap().aggregated_pubkey(), - /// ) - /// ``` - pub fn aggregated_pubkey_untweaked>(&self) -> T { - let untweaked = (self.pubkey - self.tweak_acc * G).negate_if(self.parity_acc); - T::from(untweaked.unwrap()) // Can never be infinity - } - - /// Returns the sum of all tweaks applied so far to this `KeyAggContext`. - /// Returns `None` if the tweak sum is zero i.e. if no tweaks have been - /// applied, or if the tweaks canceled each other out (by summing to zero). - pub fn tweak_sum>(&self) -> Option { - self.tweak_acc.into_option().map(T::from) - } - - /// Returns a read-only reference to the ordered set of public keys - /// which this `KeyAggContext` was created with. - pub fn pubkeys(&self) -> &[Point] { - &self.ordered_pubkeys - } - - /// Looks up the index of a given pubkey in the key aggregation group. - /// Returns `None` if the key is not a member of the group. - pub fn pubkey_index(&self, pubkey: impl Into) -> Option { - self.pubkey_indexes.get(&pubkey.into()).copied() - } - - /// Returns the public key for a given signer's index. - /// - /// Keys are best identified by their index from zero, because - /// MuSig allows more than one signer to share the same public key. - pub fn get_pubkey>(&self, index: usize) -> Option { - self.ordered_pubkeys.get(index).copied().map(T::from) - } - - /// Finds the key coefficient for a given public key. Returns `None` if - /// the given `pubkey` is not part of the aggregated key. This coefficient - /// is the same for any two copies of the same public key. - /// - /// Key coefficients are multiplicative tweaks applied to each public key - /// in an aggregated MuSig key. They prevent rogue key attacks by ensuring that - /// signers cannot effectively compute their public key as a function of the - /// pubkeys of other signers. - /// - /// The key coefficient is computed by hashing the public key `X` with a hash of - /// the ordered set of all public keys in the signing group, denoted `L`. - /// `KeyAggContext` caches these coefficients on instantiation. - pub fn key_coefficient(&self, pubkey: impl Into) -> Option { - let index = self.pubkey_index(pubkey)?; - Some(self.key_coefficients[index]) - } - - /// Finds the effective pubkey for a given individual pubkey. This is - /// essentially the same as `pubkey * key_agg_ctx.key_coefficient(pubkey)`, - /// except it is faster than recomputing it manually because the `key_agg_ctx` - /// caches this value internally. - /// - /// Returns `None` if the given `pubkey` is not part of the aggregated key. - pub fn effective_pubkey>(&self, pubkey: impl Into) -> Option { - let index = self.pubkey_index(pubkey)?; - Some(T::from(self.effective_pubkeys[index])) - } - - /// Compute the aggregated secret key for the [`KeyAggContext`] given an ordered - /// set of secret keys. Returns [`InvalidSecretKeysError`] if the secret keys do not - /// align with the ordered set of pubkeys intially given to the [`KeyAggContext`], - /// which can be checked via the [`KeyAggContext::pubkeys`] method. - pub fn aggregated_seckey>( - &self, - seckeys: impl IntoIterator, - ) -> Result { - let mut group_seckey = MaybeScalar::Zero; - for (i, seckey) in seckeys.into_iter().enumerate() { - let key_coeff = *self.key_coefficients.get(i).ok_or(InvalidSecretKeysError)?; - group_seckey += seckey * key_coeff; - } - group_seckey = group_seckey.negate_if(self.parity_acc); - - let group_tweaked_seckey = (group_seckey + self.tweak_acc).not_zero()?; - - if group_tweaked_seckey * G != self.pubkey { - return Err(InvalidSecretKeysError); - } - - Ok(T::from(group_tweaked_seckey)) - } -} - -fn hash_pubkeys>(ordered_pubkeys: &[P]) -> [u8; 32] { - let mut h = tagged_hashes::KEYAGG_LIST_TAG_HASHER.clone(); - for pubkey in ordered_pubkeys { - h.update(pubkey.borrow().serialize()); - } - h.finalize().into() -} - -fn compute_key_aggregation_coefficient( - pk_list_hash: &[u8; 32], - pubkey: &Point, - pk2: Option<&Point>, -) -> MaybeScalar { - if pk2.is_some_and(|pk2| pubkey == pk2) { - return MaybeScalar::one(); - } - - let hash: [u8; 32] = tagged_hashes::KEYAGG_COEFF_TAG_HASHER - .clone() - .chain_update(pk_list_hash) - .chain_update(pubkey.serialize()) - .finalize() - .into(); - - MaybeScalar::reduce_from(&hash) -} - -impl PartialEq for KeyAggContext { - fn eq(&self, other: &Self) -> bool { - self.ordered_pubkeys == other.ordered_pubkeys - && bool::from(self.parity_acc.ct_eq(&other.parity_acc)) - && self.tweak_acc == other.tweak_acc - } -} - -impl Eq for KeyAggContext {} - -impl BinaryEncoding for KeyAggContext { - type Serialized = Vec; - - /// Serializes a key aggregation context object into binary format. - /// - /// This is a variable-length encoding of the following fields: - /// - /// - `header_byte` (1 byte) - /// - Lowest order bit is set if the parity of the aggregated pubkey should be negated upon - /// deserialization (due to use of "x-only" tweaks). - /// - Second lowest order bit is set if there is an accumulated tweak value present in the - /// serialization. - /// - `tweak_acc` \[optional\] (32 bytes) - /// - A non-zero scalar representing the accumulated value of prior tweaks. - /// - Present only if `header_byte & 0b10 != 0`. - /// - `n_pubkey` (4 bytes) - /// - Big-endian encoded `u32`, describing the number of pubkeys which are to follow. - /// - `ordered_pubkeys` (33 * `n_pubkey` bytes) - /// - The public keys needed to reconstruct the `KeyAggContext`, in the same order in which - /// they were originally presented. - /// - /// This is a custom data format, not drawn from any standards. An identical - /// `KeyAggContext` can be reconstructed from this binary representation using - /// [`KeyAggContext::from_bytes`]. - /// - /// This is also the serialization implemented for [`serde::Serialize`] and - /// [`serde::Deserialize`] if the `serde` feature of this crate is enabled. - fn to_bytes(&self) -> Self::Serialized { - let parity_acc_bit = self.parity_acc.unwrap_u8(); - let tweak_acc_bit = u8::from(!self.tweak_acc.is_zero()); - - let n_pubkey = self.ordered_pubkeys.len(); - let total_len = 1 + 4 + (32 * (tweak_acc_bit as usize)) + (n_pubkey * 33); - - let mut serialized = Vec::::with_capacity(total_len); - - let header_byte = (tweak_acc_bit << 1) | parity_acc_bit; - serialized.push(header_byte); - - if tweak_acc_bit != 0 { - serialized.extend_from_slice(&self.tweak_acc.serialize()); - } - - serialized.extend_from_slice(&(n_pubkey as u32).to_be_bytes()); - for pubkey in self.ordered_pubkeys.iter() { - serialized.extend_from_slice(&pubkey.serialize()); - } - - serialized - } - - /// Deserializes a `KeyAggContext` from its binary serialization. - /// See [`KeyAggContext::to_bytes`] for a description of the - /// expected binary format. - fn from_bytes(bytes: &[u8]) -> Result> { - // minimum length: 1 byte header + 4 byte n_pubkey + 33 byte pubkey - if bytes.len() < 38 { - return Err(DecodeError::bad_length(bytes.len())); - } - - let header_byte = bytes[0]; - let parity_acc = subtle::Choice::from(header_byte & 1); - let mut cursor: usize = 1; - - // Decode 32-byte tweak_acc if present - let tweak_acc = if header_byte & 0b10 != 0 { - // only non-zero tweak accumulators are accepted in deserialization - let tweak_acc = Scalar::from_slice(&bytes[cursor..cursor + 32])?; - cursor += 32; - MaybeScalar::Valid(tweak_acc) - } else { - MaybeScalar::Zero - }; - - let n_pubkey_bytes = <[u8; 4]>::try_from(&bytes[cursor..cursor + 4]).unwrap(); - let n_pubkey = u32::from_be_bytes(n_pubkey_bytes) as usize; - cursor += 4; - - // wrong number of bytes remaining for the specified number of pubkeys. - if bytes.len() - cursor != n_pubkey * 33 { - return Err(DecodeError::bad_length(bytes.len())); - } - - let pubkeys: Vec = bytes[cursor..] - .chunks_exact(33) - .map(Point::from_slice) - .collect::>()?; - - let mut key_agg_ctx = KeyAggContext::new(pubkeys)?; - key_agg_ctx.parity_acc = parity_acc; - - if bool::from(parity_acc) { - key_agg_ctx.pubkey = -key_agg_ctx.pubkey; - } - - match tweak_acc { - MaybeScalar::Zero => Ok(key_agg_ctx), - MaybeScalar::Valid(t) => Ok(key_agg_ctx.with_plain_tweak(t)?), - } - } -} - -impl_encoding_traits!(KeyAggContext); -impl_hex_display!(KeyAggContext); - -#[cfg(test)] -mod tests { - use super::*; - use crate::{sign_solo, testhex, verify_single, CompactSignature}; - - #[test] - fn test_key_aggregation() { - const KEY_AGGREGATION_VECTORS: &[u8] = include_bytes!("test_vectors/key_agg_vectors.json"); - - #[derive(serde::Deserialize)] - struct ValidTestCase { - pub key_indices: Vec, - - #[serde(deserialize_with = "testhex::deserialize")] - pub expected: [u8; 32], - } - - #[derive(serde::Deserialize)] - struct KeyAggregationVectors { - #[serde(deserialize_with = "testhex::deserialize_vec")] - pub pubkeys: Vec<[u8; 33]>, - - pub valid_test_cases: Vec, - } - - let vectors: KeyAggregationVectors = serde_json::from_slice(KEY_AGGREGATION_VECTORS) - .expect("failed to load key aggregation test vectors"); - - for test_case in vectors.valid_test_cases { - let pubkeys: Vec = test_case - .key_indices - .into_iter() - .map(|i| { - Point::try_from(&vectors.pubkeys[i]) - .expect("failed to parse valid public key string") - }) - .collect(); - - let aggregated_pubkey: Point = KeyAggContext::new(pubkeys) - .expect("failed to aggregated valid pubkeys") - .aggregated_pubkey(); - - assert_eq!(aggregated_pubkey.serialize_xonly(), test_case.expected); - } - } - - #[test] - fn test_aggregation_context_tweaks() { - let pubkeys: [Point; 3] = [ - "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" - .parse() - .unwrap(), - "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9" - .parse() - .unwrap(), - "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" - .parse() - .unwrap(), - ]; - - let ctx = KeyAggContext::new(pubkeys) - .expect("failed to generate key aggregation context") - .with_xonly_tweak( - "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB" - .parse::() - .unwrap(), - ) - .expect("error while tweaking KeyAggContext") - .with_xonly_tweak( - "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455" - .parse::() - .unwrap(), - ) - .expect("error while tweaking KeyAggContext") - .with_plain_tweak( - "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0" - .parse::() - .unwrap(), - ) - .expect("error while tweaking KeyAggContext") - .with_plain_tweak( - "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D" - .parse::() - .unwrap(), - ) - .expect("error while tweaking KeyAggContext"); - - assert_eq!( - ctx.pubkey, - "0269434B39A026A4AAC9E6C1AEBDD3993FFA581C8F7F21B6FAAE15608057F5CE85" - .parse::() - .unwrap() - ); - assert!(bool::from(ctx.parity_acc)); - assert_eq!( - ctx.tweak_acc, - "A5BEB2D09000E2391E98EEBC8AA80CD4FB13845DC75B673D8466609410627D0B" - .parse() - .unwrap() - ); - - // Taproot tweaks - let ctx = KeyAggContext::new(pubkeys) - .expect("failed to generate key aggregation context") - .with_taproot_tweak( - &"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - .parse::() - .unwrap() - .serialize(), - ) - .expect("failed to tweak key with taproot commitment"); - assert_eq!( - ctx.pubkey, - "024650cca5e389f62e960f66ca0400927a7727fc6e84b9c38a1fd9a80271377ceb" - .parse::() - .unwrap() - ); - - // Unspendable tweaks - let ctx = KeyAggContext::new(pubkeys) - .expect("failed to generate key aggregation context") - .with_unspendable_taproot_tweak() - .expect("failed to tweak key with unspendable taproot commitment"); - assert_eq!( - ctx.pubkey, - "029a893e777979e0cb827cd3d0458b1a677ad68f3c69ad0120cf5fc9e3268401cb" - .parse::() - .unwrap() - ); - } - - #[test] - fn key_agg_ctx_serialization() { - struct KeyAggSerializationTest { - pubkeys: Vec<&'static str>, - tweaks: Vec<(&'static str, bool)>, - serialized_hex: &'static str, - } - - let serialization_tests = [ - KeyAggSerializationTest { - pubkeys: vec!["03d6f09ede845037a2396b9877bd6105be437488fad29dcac6576cdb3610f3ab66"], - tweaks: vec![], - serialized_hex: - "000000000103d6f09ede845037a2396b9877bd6105be437488fad29dcac6576cdb3610f3ab66", - }, - KeyAggSerializationTest { - pubkeys: vec![ - "0368288652632d5402d5ae3cb8d4094cb006aa2940156ff5dc4735d1445dfe1b34", - "02c65e684ab27c879f46f47064acf80f2c4590fb6edf7932a79b973246c7edd331", - "02d1b04674aaf6966af91201307b31501c56e6fdc41cd146b9e33912ec6e5182a2", - ], - tweaks: vec![], - serialized_hex: - "00000000030368288652632d5402d5ae3cb8d4094cb006aa2940156ff5dc4735d1445dfe1b\ - 3402c65e684ab27c879f46f47064acf80f2c4590fb6edf7932a79b973246c7edd33102d1b0\ - 4674aaf6966af91201307b31501c56e6fdc41cd146b9e33912ec6e5182a2", - }, - KeyAggSerializationTest { - pubkeys: vec![ - "032dd0f586175a1aa4c2fb9d01ec8d883de009d994f0db6a1f8ff75c4362e50c8a", - "03b68eede67797f8bd6b7d4adf6138942f344c2973e3e88ad254aedece82a144da", - ], - tweaks: vec![( - "79441652a4864a0545fa1588af4e8dd7895ddb45c1cf15a7e05d3e0d9fb86c9b", - true, - )], - serialized_hex: - "0279441652a4864a0545fa1588af4e8dd7895ddb45c1cf15a7e05d3e0d9fb86c9b00000002\ - 032dd0f586175a1aa4c2fb9d01ec8d883de009d994f0db6a1f8ff75c4362e50c8a03b68eed\ - e67797f8bd6b7d4adf6138942f344c2973e3e88ad254aedece82a144da", - }, - KeyAggSerializationTest { - pubkeys: vec![ - "025027dab744f11eafb6529c8f7cb4b2390883b55a76cf412bf49c5e39df755c3e", - "030413712ed74027795832e78457020aeff0e73624327c6d321737881078b780dd", - ], - tweaks: vec![ - ( - "d0f447289a190a50832e5daf723b0d01a58441ca465743d2db8d3c4baae37cc8", - false, - ), - ( - "cd9aa8433e17b3c07c3241592e62e7169aa7ee98cb14e95ec47f41ea4eda9ddf", - false, - ), - ], - serialized_hex: - "029e8eef6bd830be10ff609f08a09df419857d537c62238cf5e03a1fa92987d96600000002\ - 025027dab744f11eafb6529c8f7cb4b2390883b55a76cf412bf49c5e39df755c3e030413712\ - ed74027795832e78457020aeff0e73624327c6d321737881078b780dd", - }, - KeyAggSerializationTest { - pubkeys: vec![ - "0355d7de59c20355f9d8b14ccb60998983e9d73b38f64c9b9f2c4f868c0a7ac02f", - "039582f6f17f99784bc6de6e5664ef5f69eb1bf0dc151d824b19481ab0717c0cd5", - ], - tweaks: vec![ - ( - "55542efaf2708bbde8d36dec4b2ac4a698d30d2320ea4e373e31b79d803a8633", - true, - ), - ( - "09a7200a86d56e24ca0d23b64eb25ba4458b2834ceaa6506319e10e4e605e2db", - false, - ), - ( - "5dc0cf7ce9bf937ccbf6167d0c1ba02b9ae2a615ff52142e3b932d332ab699f4", - true, - ), - ( - "42cc20f9df78fc1ca2fa83d03942bae507f74716d68304d008c54eade89cafd4", - false, - ), - ], - serialized_hex: - "034191a1714ff295b6bc1008aaab813ac5c47bb7d4e64065c0d488b35ead12e0ba\ - 000000020355d7de59c20355f9d8b14ccb60998983e9d73b38f64c9b9f2c4f868c\ - 0a7ac02f039582f6f17f99784bc6de6e5664ef5f69eb1bf0dc151d824b19481ab0717c0cd5", - }, - KeyAggSerializationTest { - pubkeys: vec![ - "0317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc5", - "02947f02de710d51280b861c101bcee4e06f09a5a119694677818dce59354b62a8", - "023b89ea0ef047b6f6a2aa826e869c9538fe2f011f4df5a5422af4c24c19f22856", - ], - tweaks: vec![ - ( - "ffa540e2d3df158dfb202fc1a2cbb20c4920ba35e8f75bb11101bfa47d71449a", - true, - ), - ( - "fdc5d9e884851a8a5dd1e8c2015b15e9aed45807d05eea1b897421770351e09e", - true, - ), - ( - "2743a21ac21cc46843e478ce094663c08103f9ab88c53850f4b3280ded4d75c1", - true, - ), - ], - serialized_hex: - "0229d8874f69b8944feaf2604a651f9bc7fe6ca13b2e0032fbd9e2040c0cf6d30b0000000\ - 30317aec4eea8a2b02c38e6b67c26015d16c82a3a44abc28d1def124c1f79786fc502947f\ - 02de710d51280b861c101bcee4e06f09a5a119694677818dce59354b62a8023b89ea0ef04\ - 7b6f6a2aa826e869c9538fe2f011f4df5a5422af4c24c19f22856", - }, - ]; - - for test_case in serialization_tests { - let pubkeys: Vec = test_case - .pubkeys - .into_iter() - .map(|s| s.parse().unwrap()) - .collect(); - - let tweaks: Vec<(Scalar, bool)> = test_case - .tweaks - .into_iter() - .map(|(s, is_xonly)| (s.parse().unwrap(), is_xonly)) - .collect(); - - let expected_serialization = - base16ct::mixed::decode_vec(test_case.serialized_hex).unwrap(); - - let key_agg_ctx = KeyAggContext::new(pubkeys) - .unwrap() - .with_tweaks(tweaks) - .unwrap(); - - let serialized_ctx = key_agg_ctx.to_bytes(); - assert_eq!( - serialized_ctx, expected_serialization, - "serialized KeyAggContext does not match expected" - ); - - let deserialized_ctx = KeyAggContext::from_bytes(&serialized_ctx) - .expect("error deserializing KeyAggContext"); - - assert_eq!( - deserialized_ctx, key_agg_ctx, - "deserialized KeyAggContext does not match original" - ); - - // Test serde deserialization - let _: KeyAggContext = - serde_json::from_str(&format!("\"{}\"", test_case.serialized_hex)) - .expect("failed to deserialize KeyAggContext with serde"); - } - } - - // The test is repeated to catch failures caused by keys whose - // parity randomly align to make incorrect parity-handling code succeed. - #[test] - fn secret_key_aggregation_random() { - let mut rng = rand::thread_rng(); - for _ in 0..16 { - let seckeys = [ - Scalar::random(&mut rng), - Scalar::random(&mut rng), - Scalar::random(&mut rng), - Scalar::random(&mut rng), - ]; - - let pubkeys: Vec = seckeys - .into_iter() - .map(|seckey| seckey.base_point_mul()) - .collect(); - - // Without tweak - { - let key_agg_ctx = KeyAggContext::new(pubkeys.clone()).unwrap(); - let group_seckey: Scalar = key_agg_ctx.aggregated_seckey(seckeys).unwrap(); - let group_pubkey: Point = key_agg_ctx.aggregated_pubkey(); - assert_eq!(group_seckey.base_point_mul(), group_pubkey); - - let message = b"hello world"; - let signature: CompactSignature = sign_solo(group_seckey, message, &mut rng); - - verify_single(group_pubkey, signature, message) - .expect("tweaked signature as group should be valid"); - } - - // With a tweak - { - let key_agg_ctx = KeyAggContext::new(pubkeys.clone()) - .unwrap() - .with_unspendable_taproot_tweak() - .unwrap(); - - let group_seckey: Scalar = key_agg_ctx.aggregated_seckey(seckeys).unwrap(); - let group_pubkey: Point = key_agg_ctx.aggregated_pubkey(); - assert_eq!(group_seckey.base_point_mul(), group_pubkey); - - let message = b"hello world"; - let signature: CompactSignature = sign_solo(group_seckey, message, &mut rng); - - verify_single(group_pubkey, signature, message) - .expect("tweaked signature as group should be valid"); - } - } - } -} diff --git a/crates/musig2/src/key_sort.rs b/crates/musig2/src/key_sort.rs deleted file mode 100644 index 6e2ad430..00000000 --- a/crates/musig2/src/key_sort.rs +++ /dev/null @@ -1,22 +0,0 @@ -#[cfg(test)] -mod tests { - use secp::Point; - - #[test] - fn test_sort_public_keys() { - const KEY_SORT_VECTORS: &[u8] = include_bytes!("test_vectors/key_sort_vectors.json"); - - #[derive(serde::Deserialize)] - struct KeySortVectors { - pubkeys: Vec, - sorted_pubkeys: Vec, - } - - let vectors: KeySortVectors = serde_json::from_slice(KEY_SORT_VECTORS) - .expect("failed to decode key_sort_vectors.json"); - - let mut pubkeys = vectors.pubkeys; - pubkeys.sort(); - assert_eq!(pubkeys, vectors.sorted_pubkeys); - } -} diff --git a/crates/musig2/src/lib.rs b/crates/musig2/src/lib.rs deleted file mode 100644 index 7c20303b..00000000 --- a/crates/musig2/src/lib.rs +++ /dev/null @@ -1,55 +0,0 @@ -#![doc = include_str!("../README.md")] -#![doc = include_str!("../doc/API.md")] -#![allow(non_snake_case)] -#![warn(missing_docs)] - -#[cfg(all(not(feature = "secp256k1"), not(feature = "k256")))] -compile_error!("At least one of the `secp256k1` or `k256` features must be enabled."); - -#[macro_use] -mod binary_encoding; - -mod bip340; -mod key_agg; -mod key_sort; -mod nonces; -mod rounds; -mod sig_agg; -mod signature; -mod signing; - -#[doc = include_str!("../doc/adaptor_signatures.md")] -pub mod adaptor { - pub use crate::{ - bip340::{sign_solo_adaptor as sign_solo, verify_single_adaptor as verify_single}, - sig_agg::aggregate_partial_adaptor_signatures as aggregate_partial_signatures, - signature::AdaptorSignature, - signing::{sign_partial_adaptor as sign_partial, verify_partial_adaptor as verify_partial}, - }; -} - -pub mod deterministic; -pub mod errors; -pub mod tagged_hashes; - -pub mod rkyv_wrappers; -pub use binary_encoding::*; -pub use bip340::{sign_solo, verify_single}; -pub use key_agg::*; -pub use nonces::*; -pub use rounds::*; -pub use sig_agg::aggregate_partial_signatures; -pub use signature::*; -pub use signing::{compute_challenge_hash_tweak, sign_partial, verify_partial, PartialSignature}; - -#[cfg(test)] -pub(crate) mod testhex; - -#[cfg(any(test, feature = "rand"))] -pub use bip340::{verify_batch, BatchVerificationRow}; -#[cfg(feature = "k256")] -pub use k256; -/// Re-export of the inner types used to represent curve points and scalars. -pub use secp; -#[cfg(feature = "secp256k1")] -pub use secp256k1; diff --git a/crates/musig2/src/nonces.rs b/crates/musig2/src/nonces.rs deleted file mode 100644 index f4974e2f..00000000 --- a/crates/musig2/src/nonces.rs +++ /dev/null @@ -1,992 +0,0 @@ -use rkyv::{Archive, Deserialize, Serialize}; -use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; -// use serde::{Deserialize, Serialize}; -use sha2::Digest as _; - -use crate::{errors::DecodeError, rkyv_wrappers, tagged_hashes, BinaryEncoding}; - -/// Represents the primary source of entropy for building a [`SecNonce`]. -/// -/// Often referred to as the variable `rand` in -/// [BIP-0327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki) and -/// [BIP-0340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) -pub struct NonceSeed(pub [u8; 32]); - -impl From<[u8; 32]> for NonceSeed { - /// Converts a byte array to a `NonceSeed` by moving. - fn from(bytes: [u8; 32]) -> Self { - NonceSeed(bytes) - } -} - -impl From<&[u8; 32]> for NonceSeed { - /// Converts a reference to a byte array to a `NonceSeed` by copying. - fn from(bytes: &[u8; 32]) -> Self { - NonceSeed(*bytes) - } -} - -#[cfg(any(test, feature = "rand"))] -impl From<&mut T> for NonceSeed { - /// This implementation draws a [`NonceSeed`] from a mutable reference - /// to a CSPRNG. Panics if the RNG fails to fill the seed with 32 - /// random bytes. - fn from(rng: &mut T) -> NonceSeed { - let mut bytes = [0u8; 32]; - rng.try_fill_bytes(&mut bytes) - .expect("error generating secure secret nonce seed"); - NonceSeed(bytes) - } -} - -pub(crate) fn xor_bytes(a: &[u8; SIZE], b: &[u8; SIZE]) -> [u8; SIZE] { - let mut out = [0; SIZE]; - for i in 0..SIZE { - out[i] = a[i] ^ b[i] - } - out -} - -fn extra_input_length_check>(extra_inputs: &[T]) { - let total_len: usize = extra_inputs - .iter() - .map(|extra_input| extra_input.as_ref().len()) - .sum(); - assert!( - total_len <= u32::MAX as usize, - "excessive use of extra_input when building secnonce; max length is 2^32 bytes" - ); -} - -/// A set of optional parameters which can be provided to _spice up_ the -/// entropy of the secret nonces generated for a signing session. -/// -/// These parameters are not functionally required for any operations after -/// nonce generation - you can provide a different secret key in the `SecNonceSpices` -/// than you'll use for actual signing, and the signature will still be valid. -/// However, using the parameters appropriately will reduce the risk of -/// your code accidentally reusing a nonce and exposing your secret key. -/// -/// This type is meant to be used as a parameter of the state-machine API available -/// via [`FirstRound`][crate::FirstRound] and [`SecondRound`][crate::SecondRound]. -/// For standalone nonce generation, see [`SecNonceBuilder`] or [`SecNonce::generate`]. -#[derive(Clone, Default)] -pub struct SecNonceSpices<'ns> { - pub(crate) seckey: Option, - pub(crate) message: Option<&'ns dyn AsRef<[u8]>>, - pub(crate) extra_inputs: Vec<&'ns dyn AsRef<[u8]>>, -} - -impl<'ns> SecNonceSpices<'ns> { - /// Creates a new empty set of `SecNonceSpices`. Same as [`SecNonceSpices::default`]. - pub fn new() -> SecNonceSpices<'ns> { - SecNonceSpices::default() - } - - /// Add the secret key you intend to sign with to the spice rack. - /// This doesn't _need_ to be the actual key you sign with, but - /// for best efficacy that would be the recommended usage. - pub fn with_seckey(self, seckey: impl Into) -> SecNonceSpices<'ns> { - SecNonceSpices { - seckey: Some(seckey.into()), - ..self - } - } - - /// Spices up the nonce with the message you intend to sign. Similarly - /// to [`SecNonceSpices::with_seckey`], this doesn't need to be the actual message - /// you end up signing, but that would help. - pub fn with_message>(self, message: &'ns M) -> SecNonceSpices<'ns> { - SecNonceSpices { - message: Some(message), - ..self - } - } - - /// Add some arbitrary extra input, any context-specific data you have on hand, to - /// spice up the nonce generation process. This method is additive, appending - /// further extra data on top of previous chunks, which will all be cumulatively - /// hashed to produce the final secret nonce. - /// - /// ``` - /// let session_id = [0x11u8; 16]; - /// - /// musig2::SecNonceSpices::new() - /// .with_extra_input(b"hello world") - /// .with_extra_input(&session_id) - /// .with_extra_input(&(42u32).to_be_bytes()); - /// ``` - pub fn with_extra_input>(mut self, extra_input: &'ns E) -> SecNonceSpices<'ns> { - self.extra_inputs.push(extra_input); - extra_input_length_check(&self.extra_inputs); - self - } -} - -/// A helper struct used to construct [`SecNonce`] instances. -/// -/// `SecNonceBuilder` allows piecemeal salting of the resulting `SecNonce` -/// depending on what is available to the caller. -/// -/// `SecNonce`s can be constructed in a variety of ways using different -/// input sources to increase their entropy. While simple random sampling -/// of `SecNonce` is acceptable in theory, RNGs can fail quietly sometimes. -/// If possible, it is highly recommended to also salt the nonce with -/// session-specific data, such as the message being signed, or the -/// public/secret key which will be used for signing. -/// -/// At bare minimum, [`SecNonceBuilder::new`] requires only 32 random -/// input bytes. Chainable methods can be used thereafter to salt the resulting -/// nonce with additional data. The nonce can be finalized and returned by -/// [`SecNonceBuilder::build`]. -/// -/// If no other data is available, we highly recommend _at least_ salting the nonce -/// with the public key, as recommended by BIP327. -/// -/// # Example -/// -/// Here we construct a nonce which we intend to use to sign the byte string -/// `b"hello world"` with a specific public key. -/// -/// ``` -/// use secp::Point; -/// -/// // in reality, this would be generated by a CSPRNG. -/// let nonce_seed = [0xAB; 32]; -/// -/// let secnonce = musig2::SecNonceBuilder::new(nonce_seed) -/// .with_pubkey( -/// "037eaef9ce945fbcef58c6ca818f433fad8275c09441b06a274a93aa5d69374f62" -/// .parse::() -/// .expect("fail"), -/// ) -/// .with_message(b"hello world") -/// .build(); -/// -/// assert_eq!( -/// secnonce, -/// "304e472f8028efc386eb305b496e49a9c71984fbddb915c04002764a98d77a82\ -/// b2f29921753a6a05a1f91556debdaac4d20ad20519f91bcebf4a2d842a05b0bc" -/// .parse() -/// .unwrap() -/// ); -/// ``` -pub struct SecNonceBuilder<'snb> { - nonce_seed_bytes: [u8; 32], - seckey: Option, - pubkey: Option, - aggregated_pubkey: Option, - message: Option<&'snb [u8]>, - extra_inputs: Vec<&'snb dyn AsRef<[u8]>>, -} - -impl<'snb> SecNonceBuilder<'snb> { - /// Start building a nonce, seeded with the given random data - /// source `nonce_seed`, which should either be - /// - /// - 32 bytes drawn from a cryptographically secure RNG, OR - /// - a mutable reference to a secure RNG. - /// - /// ``` - /// use rand::RngCore as _; - /// - /// # #[cfg(feature = "rand")] - /// // Sample the seed automatically - /// let secnonce = musig2::SecNonceBuilder::new(&mut rand::rngs::OsRng) - /// .with_message(b"hello world!") - /// .build(); - /// - /// // Sample the seed manually - /// let mut nonce_seed = [0u8; 32]; - /// rand::rngs::OsRng.fill_bytes(&mut nonce_seed); - /// let secnonce = musig2::SecNonceBuilder::new(nonce_seed) - /// .with_message(b"hello world!") - /// .build(); - /// ``` - /// - /// # WARNING - /// - /// It is critical for the `nonce_seed` to be **sampled randomly,** and NOT - /// constructed deterministically based on signing session data. Otherwise, - /// the signer can be [tricked into reusing the same nonce for concurrent - /// signing sessions, thus exposing their secret key.]( - #[doc = "https://medium.com/blockstream/musig-dn-schnorr-multisignatures\ - -with-verifiably-deterministic-nonces-27424b5df9d6#e3b6)"] - pub fn new(nonce_seed: impl Into) -> SecNonceBuilder<'snb> { - let NonceSeed(nonce_seed_bytes) = nonce_seed.into(); - SecNonceBuilder { - nonce_seed_bytes, - seckey: None, - pubkey: None, - aggregated_pubkey: None, - message: None, - extra_inputs: Vec::new(), - } - } - - /// Salt the resulting nonce with the public key expected to be used - /// during the signing phase. - /// - /// The public key will be overwritten if [`SecNonceBuilder::with_seckey`] - /// is used after this method. - pub fn with_pubkey(self, pubkey: impl Into) -> SecNonceBuilder<'snb> { - SecNonceBuilder { - pubkey: Some(pubkey.into()), - ..self - } - } - - /// Salt the resulting nonce with the secret key which the nonce should be - /// used to protect during the signing phase. - /// - /// Overwrites any public key previously added by - /// [`SecNonceBuilder::with_pubkey`], as we compute the public key - /// of the given secret key and add it to the builder. - pub fn with_seckey(self, seckey: impl Into) -> SecNonceBuilder<'snb> { - let seckey: Scalar = seckey.into(); - SecNonceBuilder { - seckey: Some(seckey), - pubkey: Some(seckey * G), - ..self - } - } - - /// Salt the resulting nonce with the message which we expect to be signing with - /// the nonce. - pub fn with_message>(self, msg: &'snb M) -> SecNonceBuilder<'snb> { - SecNonceBuilder { - message: Some(msg.as_ref()), - ..self - } - } - - /// Salt the resulting nonce with the aggregated public key which we expect to aggregate - /// signatures for. - pub fn with_aggregated_pubkey( - self, - aggregated_pubkey: impl Into, - ) -> SecNonceBuilder<'snb> { - SecNonceBuilder { - aggregated_pubkey: Some(aggregated_pubkey.into()), - ..self - } - } - - /// Salt the resulting nonce with arbitrary extra input bytes. This might be context-specific - /// data like a signing session ID, the name of the protocol, the current timestamp, whatever - /// you want, really. - /// - /// This method is additive; it does not overwrite the `extra_input` values added by previous - /// invocations of itself. This allows the caller to salt the nonce with an arbitrary amount - /// of extra entropy as desired, up to a limit of [`u32::MAX`] bytes (about 4GB). This method - /// will panic if the sum of all extra inputs attached to the builder would exceed that limit. - /// - /// ``` - /// # let nonce_seed = [0xABu8; 32]; - /// let remote_ip = [127u8, 0, 0, 1]; - /// - /// let secnonce = musig2::SecNonceBuilder::new(nonce_seed) - /// .with_extra_input(b"MyApp") - /// .with_extra_input(&remote_ip) - /// .with_extra_input(&String::from("What's up buttercup?")) - /// .build(); - /// ``` - pub fn with_extra_input>( - mut self, - extra_input: &'snb E, - ) -> SecNonceBuilder<'snb> { - self.extra_inputs.push(extra_input); - extra_input_length_check(&self.extra_inputs); - self - } - - /// Sprinkles in a set of [`SecNonceSpices`] to this nonce builder. Extra inputs in - /// `spices` are appended to the builder (see [`SecNonceBuilder::with_extra_input`]). - /// All other parameters will be merged with those in `spices`, preferring parameters - /// in `spices` if they are present. - pub fn with_spices(mut self, spices: SecNonceSpices<'snb>) -> SecNonceBuilder<'snb> { - self.seckey = spices.seckey.or(self.seckey); - self.message = spices.message.map(|msg| msg.as_ref()).or(self.message); - - let mut new_extra_inputs = spices.extra_inputs; - self.extra_inputs.append(&mut new_extra_inputs); - extra_input_length_check(&self.extra_inputs); - - self - } - - /// Build the secret nonce by hashing all of the builder's inputs into two - /// byte arrays, and reducing those byte arrays modulo the curve order into - /// two scalars `k1` and `k2`. These form the `SecNonce` as the tuple `(k1, k2)`. - /// - /// If the reduction results in an output of zero for either scalar, - /// we use a nonce of 1 instead for that scalar. - /// - /// This method matches the standard nonce generation algorithm specified in - /// [BIP327](https://github.com/bitcoin/bips/blob/master/bip-0327.mediawiki), - /// except in the extremely unlikely case of a hash reducing to zero. - pub fn build(self) -> SecNonce { - let seckey_bytes = match self.seckey { - Some(seckey) => seckey.serialize(), - None => [0u8; 32], - }; - - let nonce_seed_hash: [u8; 32] = tagged_hashes::MUSIG_AUX_TAG_HASHER - .clone() - .chain_update(self.nonce_seed_bytes) - .finalize() - .into(); - - let mut hasher = tagged_hashes::MUSIG_NONCE_TAG_HASHER - .clone() - .chain_update(xor_bytes(&seckey_bytes, &nonce_seed_hash)); - - // BIP327 doesn't allow the public key to be an optional argument, - // but there is no hard reason for that other than 'the RNG might fail'. - // For ergonomics we allow the pubkey to be omitted here in the same - // fashion as the aggregated pubkey. - match self.pubkey { - None => hasher.update([0]), - Some(pubkey) => { - hasher.update([33]); // individual pubkey len - hasher.update(pubkey.serialize()); - } - } - - match self.aggregated_pubkey { - None => hasher.update([0]), - Some(aggregated_pubkey) => { - hasher.update([32]); // aggregated pubkey len - hasher.update(aggregated_pubkey.serialize_xonly()); - } - }; - - match self.message { - None => hasher.update([0]), - Some(message) => { - hasher.update([1]); - hasher.update((message.len() as u64).to_be_bytes()); - hasher.update(message); - } - }; - - // We still write the extra input length if the caller provided empty extra info. - if !self.extra_inputs.is_empty() { - let extra_input_total_len: usize = self - .extra_inputs - .iter() - .map(|extra_in| extra_in.as_ref().len()) - .sum(); - - hasher.update((extra_input_total_len as u32).to_be_bytes()); - for extra_input in self.extra_inputs { - hasher.update(extra_input.as_ref()); - } - } - - // Cloning the hash engine state reduces the computations needed. - let hash1 = <[u8; 32]>::from(hasher.clone().chain_update([0]).finalize()); - let hash2 = <[u8; 32]>::from(hasher.clone().chain_update([1]).finalize()); - - let k1 = match MaybeScalar::reduce_from(&hash1) { - MaybeScalar::Zero => Scalar::one(), - MaybeScalar::Valid(k) => k, - }; - let k2 = match MaybeScalar::reduce_from(&hash2) { - MaybeScalar::Zero => Scalar::one(), - MaybeScalar::Valid(k) => k, - }; - SecNonce { k1, k2 } - } -} - -/// A pair of secret nonce scalars, used to conceal a secret key when -/// signing a message. -/// -/// The secret nonce provides randomness, blinding a signer's private key when -/// signing. It is imperative that the same `SecNonce` is not used to sign more -/// than one message with the same key, as this would allow an observer to -/// compute the private key used to create both signatures. -/// -/// `SecNonce`s can be constructed in a variety of ways using different -/// input sources to increase their entropy. See [`SecNonceBuilder`] and -/// [`SecNonce::build`] to explore secure nonce generation using -/// contextual entropy sources. -/// -/// Ideally, `SecNonce`s should be generated with a cryptographically secure -/// random number generator via [`SecNonce::generate`]. -#[derive(Debug, Eq, PartialEq, Clone, Archive, Serialize, Deserialize)] -pub struct SecNonce { - #[rkyv(with = rkyv_wrappers::Scalar)] - pub(crate) k1: Scalar, - #[rkyv(with = rkyv_wrappers::Scalar)] - pub(crate) k2: Scalar, -} - -impl SecNonce { - /// Construct a new `SecNonce` from the given individual nonce values. - pub fn new>(k1: T, k2: T) -> SecNonce { - SecNonce { - k1: k1.into(), - k2: k2.into(), - } - } - - /// Constructs a new [`SecNonceBuilder`] from the given random nonce seed. - /// - /// See [`SecNonceBuilder::new`]. - pub fn build<'snb>(nonce_seed: impl Into) -> SecNonceBuilder<'snb> { - SecNonceBuilder::new(nonce_seed) - } - - /// Generates a `SecNonce` securely from the given input arguments. - /// - /// - `nonce_seed`: the primary source of entropy used to generate the nonce. Can be any type - /// that converts to [`NonceSeed`], such as [`&mut rand::rngs::OsRng`][rand::rngs::OsRng] or - /// `[u8; 32]`. - /// - `seckey`: the secret key which will be used to sign the message. - /// - `aggregated_pubkey`: the aggregated public key. - /// - `message`: the message which will be signed. - /// - `extra_input`: arbitrary context data used to increase the entropy of the resulting - /// nonces. - /// - /// This implementation matches the specfication of nonce generation in BIP327, - /// and all arguments are required. If you cannot supply all arguments - /// to the nonce generation algorithm, use [`SecNonceBuilder`]. - /// - /// Panics if the extra input length is greater than [`u32::MAX`]. - pub fn generate( - nonce_seed: impl Into, - seckey: impl Into, - aggregated_pubkey: impl Into, - message: impl AsRef<[u8]>, - extra_input: impl AsRef<[u8]>, - ) -> SecNonce { - Self::build(nonce_seed) - .with_seckey(seckey) - .with_aggregated_pubkey(aggregated_pubkey) - .with_message(&message) - .with_extra_input(&extra_input) - .build() - } - - /// Samples a random pair of secret nonces directly from a CSPRNG. - /// - /// Whenever possible, we recommended to use [`SecNonce::generate`] or - /// [`SecNonceBuilder`] instead of this method. If the RNG fails silently for - /// any reason, it may result in duplicate `SecNonce` values, which will lead - /// to private key exposure if this same nonce is used in more than one signing - /// session. - /// - /// [`SecNonce::generate`] is more secure because it combines multiple sources of - /// entropy to compute the final nonce. - #[cfg(any(test, feature = "rand"))] - pub fn random(rng: &mut R) -> SecNonce - where - R: rand::RngCore + rand::CryptoRng, - { - SecNonce { - k1: Scalar::random(rng), - k2: Scalar::random(rng), - } - } - - /// Returns the corresponding public nonce for this secret nonce. The public nonce - /// is safe to share with other signers. - pub fn public_nonce(&self) -> PubNonce { - PubNonce { - R1: self.k1 * G, - R2: self.k2 * G, - } - } -} - -/// Represents a public nonce derived from a secret nonce. It is composed -/// of two public points, `R1` and `R2`, derived by base-point multiplying -/// the two scalars in a `SecNonce`. -/// -/// `PubNonce` can be derived from a [`SecNonce`] using [`SecNonce::public_nonce`], -/// or it can be constructed manually with [`PubNonce::new`]. -#[derive(Debug, Eq, PartialEq, Clone, Hash, Ord, PartialOrd, Archive, Serialize, Deserialize)] -pub struct PubNonce { - #[allow(missing_docs)] - #[rkyv(with = rkyv_wrappers::Point)] - pub R1: Point, - #[allow(missing_docs)] - #[rkyv(with = rkyv_wrappers::Point)] - pub R2: Point, -} - -impl PubNonce { - /// Construct a new `PubNonce` from the given pair of public nonce points. - pub fn new>(R1: T, R2: T) -> PubNonce { - PubNonce { - R1: R1.into(), - R2: R2.into(), - } - } -} - -/// Represents a aggregate sum of public nonces derived from secret nonces. -/// -/// `AggNonce` can be created by summing a collection of `PubNonce` points -/// by using [`AggNonce::sum`] or by making use of the -/// [`std::iter::Sum`](#impl-Sum

-for-AggNonce) implementation. An aggregated -/// nonce can also be constructed directly by using [`AggNonce::new`]. -/// -/// An aggregated nonce's points are allowed to be infinity (AKA the zero point). -/// If this occurs, then likely at least one signer is being mischevious. -/// To allow honest signers to identify those responsible, signing is allowed -/// to continue, and dishonest signers will reveal themselves once they are -/// required to provide their partial signatures. -#[derive(Debug, Eq, PartialEq, Clone, Hash, Ord, PartialOrd, Archive, Serialize, Deserialize)] -pub struct AggNonce { - #[allow(missing_docs)] - #[rkyv(with = rkyv_wrappers::MaybePoint)] - pub R1: MaybePoint, - #[allow(missing_docs)] - #[rkyv(with = rkyv_wrappers::MaybePoint)] - pub R2: MaybePoint, -} - -impl AggNonce { - /// Construct a new `AggNonce` from the given pair of public nonce points. - pub fn new>(R1: T, R2: T) -> AggNonce { - AggNonce { - R1: R1.into(), - R2: R2.into(), - } - } - - /// Aggregates many partial public nonces together into an aggregated nonce. - /// - /// ``` - /// use musig2::{AggNonce, PubNonce}; - /// - /// let nonces: [PubNonce; 2] = [ - /// "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798\ - /// 032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE93" - /// .parse() - /// .unwrap(), - /// "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61\ - /// 037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9" - /// .parse() - /// .unwrap(), - /// ]; - /// - /// let expected = "02aebee092fe428c3b4c53993c3f80eecbf88ca935469b5bfcaabecb7b2afbb1a6\ - /// 03c923248ac1f639368bc82345698dfb445dca6024b9ba5a9bafe971bb5813964b" - /// .parse::() - /// .unwrap(); - /// - /// assert_eq!(musig2::AggNonce::sum(&nonces), expected); - /// assert_eq!(musig2::AggNonce::sum(nonces), expected); - /// ``` - pub fn sum(nonces: I) -> AggNonce - where - T: std::borrow::Borrow, - I: IntoIterator, - { - let (r1s, r2s): (Vec, Vec) = nonces - .into_iter() - .map(|pubnonce| (pubnonce.borrow().R1, pubnonce.borrow().R2)) - .unzip(); - - AggNonce { - R1: Point::sum(r1s), - R2: Point::sum(r2s), - } - } - - /// Computes the nonce coefficient `b`, used to create the final nonce and signatures. - /// - /// Most use-cases will not need to invoke this method. Instead use - /// [`sign_solo`][crate::sign_solo] or [`sign_partial`][crate::sign_partial] - /// to create signatures. - pub fn nonce_coefficient( - &self, - aggregated_pubkey: impl Into, - message: impl AsRef<[u8]>, - ) -> S - where - S: From, - { - let hash: [u8; 32] = tagged_hashes::MUSIG_NONCECOEF_TAG_HASHER - .clone() - .chain_update(self.R1.serialize()) - .chain_update(self.R2.serialize()) - .chain_update(aggregated_pubkey.into().serialize_xonly()) - .chain_update(message.as_ref()) - .finalize() - .into(); - - S::from(MaybeScalar::reduce_from(&hash)) - } - - /// Computes the final public nonce point, published with the aggregated signature. - /// If this point winds up at infinity (probably due to a mischevious signer), we - /// instead return the generator point `G`. - /// - /// Most use-cases will not need to invoke this method. Instead use - /// [`sign_solo`][crate::sign_solo] or [`sign_partial`][crate::sign_partial] - /// to create signatures. - pub fn final_nonce

(&self, nonce_coeff: impl Into) -> P - where - P: From, - { - let nonce_coeff: MaybeScalar = nonce_coeff.into(); - let aggnonce_sum = self.R1 + (nonce_coeff * self.R2); - P::from(match aggnonce_sum { - MaybePoint::Infinity => Point::generator(), - MaybePoint::Valid(p) => p, - }) - } -} - -mod encodings { - use super::*; - - impl BinaryEncoding for SecNonce { - type Serialized = [u8; 64]; - - /// Returns the binary serialization of `SecNonce`, which serializes - /// both inner scalar values into a fixed-length 64-byte array. - /// - /// Note that this serialization differs from the format suggested - /// in BIP327, in that we do not include a public key. - fn to_bytes(&self) -> Self::Serialized { - let mut serialized = [0u8; 64]; - serialized[..32].clone_from_slice(&self.k1.serialize()); - serialized[32..].clone_from_slice(&self.k2.serialize()); - serialized - } - - /// Parses a `SecNonce` from a serialized byte slice. - /// This byte slice should be 64 bytes long, and encode two - /// non-zero 256-bit scalars. - /// - /// We also accept 97-byte long slices, to be compatible with BIP327's - /// suggested serialization format of `SecNonce`. - fn from_bytes(bytes: &[u8]) -> Result> { - if bytes.len() != 64 && bytes.len() != 97 { - return Err(DecodeError::bad_length(bytes.len())); - } - let k1 = Scalar::from_slice(&bytes[..32])?; - let k2 = Scalar::from_slice(&bytes[32..64])?; - Ok(SecNonce { k1, k2 }) - } - } - - impl BinaryEncoding for PubNonce { - type Serialized = [u8; 66]; - - /// Returns the binary serialization of `PubNonce`, which serializes - /// both inner points into a fixed-length 66-byte array. - fn to_bytes(&self) -> Self::Serialized { - let mut bytes = [0u8; 66]; - bytes[..33].clone_from_slice(&self.R1.serialize()); - bytes[33..].clone_from_slice(&self.R2.serialize()); - bytes - } - - /// Parses a `PubNonce` from a serialized byte slice. This byte slice should - /// be 66 bytes long, and encode two compressed, non-infinity curve points. - fn from_bytes(bytes: &[u8]) -> Result> { - if bytes.len() != 66 { - return Err(DecodeError::bad_length(bytes.len())); - } - let R1 = Point::from_slice(&bytes[..33])?; - let R2 = Point::from_slice(&bytes[33..])?; - Ok(PubNonce { R1, R2 }) - } - } - - impl BinaryEncoding for AggNonce { - type Serialized = [u8; 66]; - - /// Returns the binary serialization of `AggNonce`, which serializes - /// both inner points into a fixed-length 66-byte array. - fn to_bytes(&self) -> Self::Serialized { - let mut serialized = [0u8; 66]; - serialized[..33].clone_from_slice(&self.R1.serialize()); - serialized[33..].clone_from_slice(&self.R2.serialize()); - serialized - } - - /// Parses an `AggNonce` from a serialized byte slice. This byte slice should - /// be 66 bytes long, and encode two compressed (possibly infinity) curve points. - fn from_bytes(bytes: &[u8]) -> Result> { - if bytes.len() != 66 { - return Err(DecodeError::bad_length(bytes.len())); - } - let R1 = MaybePoint::from_slice(&bytes[..33])?; - let R2 = MaybePoint::from_slice(&bytes[33..])?; - Ok(AggNonce { R1, R2 }) - } - } - - impl_encoding_traits!(SecNonce, 64, 97); - impl_encoding_traits!(PubNonce, 66); - impl_encoding_traits!(AggNonce, 66); - - // Do not implement Display for SecNonce. - impl_hex_display!(PubNonce); - impl_hex_display!(AggNonce); -} - -impl

std::iter::Sum

for AggNonce -where - P: std::borrow::Borrow, -{ - /// Implements summation of partial public nonces into an aggregated nonce. - /// - /// ``` - /// use musig2::{AggNonce, PubNonce}; - /// - /// let nonces = [ - /// "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798\ - /// 032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE93" - /// .parse::() - /// .unwrap(), - /// "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61\ - /// 037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9" - /// .parse::() - /// .unwrap(), - /// ]; - /// - /// let expected = "02aebee092fe428c3b4c53993c3f80eecbf88ca935469b5bfcaabecb7b2afbb1a6\ - /// 03c923248ac1f639368bc82345698dfb445dca6024b9ba5a9bafe971bb5813964b" - /// .parse::() - /// .unwrap(); - /// - /// assert_eq!(nonces.iter().sum::(), expected); - /// assert_eq!(nonces.into_iter().sum::(), expected); - /// ``` - fn sum(iter: I) -> Self - where - I: Iterator, - { - let refs = iter.collect::>(); - AggNonce::sum(refs.iter().map(|nonce| nonce.borrow())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{testhex, KeyAggContext}; - - #[test] - fn test_nonce_generation() { - const NONCE_GEN_VECTORS: &[u8] = include_bytes!("test_vectors/nonce_gen_vectors.json"); - - #[derive(serde::Deserialize)] - struct NonceGenTestCase { - #[serde(rename = "rand", deserialize_with = "testhex::deserialize")] - nonce_seed: [u8; 32], - - #[serde(rename = "sk")] - seckey: Scalar, - - #[serde(rename = "aggpk", deserialize_with = "testhex::deserialize")] - aggregated_pubkey: [u8; 32], - - #[serde(rename = "msg", deserialize_with = "testhex::deserialize")] - message: Vec, - - #[serde(rename = "extra_in", deserialize_with = "testhex::deserialize")] - extra_input: Vec, - - expected_secnonce: SecNonce, - expected_pubnonce: PubNonce, - } - - #[derive(serde::Deserialize)] - struct NonceGenVectors { - test_cases: Vec, - } - - let vectors: NonceGenVectors = serde_json::from_slice(NONCE_GEN_VECTORS) - .expect("failed to parse test vectors from nonce_gen_vectors.json"); - - for test_case in vectors.test_cases { - let aggregated_pubkey = - Point::lift_x(&test_case.aggregated_pubkey).unwrap_or_else(|_| { - panic!( - "invalid aggregated xonly pubkey in test vector: {}", - base16ct::lower::encode_string(&test_case.aggregated_pubkey) - ) - }); - let secnonce = SecNonce::generate( - test_case.nonce_seed, - test_case.seckey, - aggregated_pubkey, - &test_case.message, - &test_case.extra_input, - ); - - assert_eq!(secnonce, test_case.expected_secnonce); - assert_eq!(secnonce.public_nonce(), test_case.expected_pubnonce); - } - } - - #[test] - fn test_nonce_aggregation() { - const NONCE_AGG_VECTORS: &[u8] = include_bytes!("test_vectors/nonce_agg_vectors.json"); - - #[derive(serde::Deserialize)] - struct NonceAggError { - signer: usize, - } - - #[derive(serde::Deserialize)] - struct NonceAggErrorTestCase { - #[serde(rename = "pnonce_indices")] - public_nonce_indexes: Vec, - error: NonceAggError, - } - - #[derive(serde::Deserialize)] - struct ValidNonceAggTestCase { - #[serde(rename = "pnonce_indices")] - public_nonce_indexes: Vec, - #[serde(rename = "expected")] - aggregated_nonce: AggNonce, - } - - #[derive(serde::Deserialize)] - struct NonceAggTestVectors { - #[serde(deserialize_with = "testhex::deserialize_vec", rename = "pnonces")] - public_nonces: Vec>, - - valid_test_cases: Vec, - error_test_cases: Vec, - } - - let vectors: NonceAggTestVectors = serde_json::from_slice(NONCE_AGG_VECTORS) - .expect("failed to parse test vectors from nonce_agg_vectors.json"); - - for test_case in vectors.valid_test_cases { - let nonces: Vec = test_case - .public_nonce_indexes - .into_iter() - .map(|i| { - PubNonce::from_bytes(&vectors.public_nonces[i]).unwrap_or_else(|_| { - panic!( - "used invalid nonce in valid test case: {}", - base16ct::lower::encode_string(&vectors.public_nonces[i]) - ) - }) - }) - .collect(); - - let aggregated_nonce = AggNonce::sum(&nonces); - - assert_eq!(aggregated_nonce, test_case.aggregated_nonce); - } - - for test_case in vectors.error_test_cases { - for (signer_index, i) in test_case.public_nonce_indexes.into_iter().enumerate() { - let nonce_result = PubNonce::try_from(vectors.public_nonces[i].as_slice()); - if signer_index == test_case.error.signer { - assert_eq!( - nonce_result, - Err(DecodeError::from(secp::errors::InvalidPointBytes)) - ); - } else { - nonce_result.unwrap_or_else(|_| { - panic!("unexpected pub nonce parsing error for signer {}", i) - }); - } - } - } - } - - #[test] - fn nonce_reuse_demo() { - let alice_seckey = Scalar::try_from([0x11; 32]).unwrap(); - let bob_seckey = Scalar::try_from([0x22; 32]).unwrap(); - - let alice_pubkey = alice_seckey * G; - let bob_pubkey = bob_seckey * G; - - let key_agg_ctx = KeyAggContext::new([alice_pubkey, bob_pubkey]).unwrap(); - - let message = b"you betta not sign this twice"; - - let alice_secnonce = SecNonceBuilder::new([0xAA; 32]).build(); - let bob_secnonce_1 = SecNonceBuilder::new([0xB1; 32]).build(); - let bob_secnonce_2 = SecNonceBuilder::new([0xB2; 32]).build(); - let bob_secnonce_3 = SecNonceBuilder::new([0xB3; 32]).build(); - - // First signature - let aggnonce_1 = - AggNonce::sum([alice_secnonce.public_nonce(), bob_secnonce_1.public_nonce()]); - let s1: MaybeScalar = crate::sign_partial( - &key_agg_ctx, - alice_seckey, - alice_secnonce.clone(), - &aggnonce_1, - message, - ) - .unwrap(); - - // Second signature - let aggnonce_2 = - AggNonce::sum([alice_secnonce.public_nonce(), bob_secnonce_2.public_nonce()]); - let s2: MaybeScalar = crate::sign_partial( - &key_agg_ctx, - alice_seckey, - alice_secnonce.clone(), - &aggnonce_2, - message, - ) - .unwrap(); - - // Third signature - let aggnonce_3 = - AggNonce::sum([alice_secnonce.public_nonce(), bob_secnonce_3.public_nonce()]); - let s3: MaybeScalar = crate::sign_partial( - &key_agg_ctx, - alice_seckey, - alice_secnonce.clone(), - &aggnonce_3, - message, - ) - .unwrap(); - - // Alice gives Bob `(s1, s2, s3)`. - // Bob can now compute Alice's secret key. - let a = key_agg_ctx.key_coefficient(alice_pubkey).unwrap(); - let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); - - let b1: MaybeScalar = aggnonce_1.nonce_coefficient(aggregated_pubkey, message); - let b2: MaybeScalar = aggnonce_2.nonce_coefficient(aggregated_pubkey, message); - let b3: MaybeScalar = aggnonce_3.nonce_coefficient(aggregated_pubkey, message); - - let e1: MaybeScalar = crate::compute_challenge_hash_tweak( - &aggnonce_1.final_nonce::(b1).serialize_xonly(), - &key_agg_ctx.aggregated_pubkey(), - message, - ); - let e2: MaybeScalar = crate::compute_challenge_hash_tweak( - &aggnonce_2.final_nonce::(b2).serialize_xonly(), - &key_agg_ctx.aggregated_pubkey(), - message, - ); - let e3: MaybeScalar = crate::compute_challenge_hash_tweak( - &aggnonce_3.final_nonce::(b3).serialize_xonly(), - &key_agg_ctx.aggregated_pubkey(), - message, - ); - - let b2_diff = (b2 - b1).unwrap(); - let b3_diff = (b3 - b1).unwrap(); - - let top = (s3 - s1) * b2_diff - (s2 - s1) * b3_diff; - let bottom = a * ((e3 - e1) * b2_diff + (e1 - e2) * b3_diff); - let extracted_key = (top / bottom.unwrap()).unwrap(); - - assert_eq!(extracted_key, alice_seckey); - } -} diff --git a/crates/musig2/src/rkyv_wrappers.rs b/crates/musig2/src/rkyv_wrappers.rs deleted file mode 100644 index d864ffb0..00000000 --- a/crates/musig2/src/rkyv_wrappers.rs +++ /dev/null @@ -1,190 +0,0 @@ -use rkyv::{Archive, Deserialize, Serialize}; -use secp256k1::ffi::CPtr; - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp::Point, derive(Hash, PartialEq, Eq))] -pub struct Point { - #[rkyv(getter = point_inner_getter, with = PublicKey)] - inner: secp256k1::PublicKey, -} - -fn point_inner_getter(p: &secp::Point) -> secp256k1::PublicKey { - p.clone().into() -} - -impl From for secp::Point { - fn from(value: Point) -> Self { - Self::from(value.inner) - } -} - -impl From for Point { - fn from(value: secp::Point) -> Self { - Self { - inner: value.into(), - } - } -} - -#[derive( - Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Archive, Serialize, Deserialize, -)] -#[rkyv(remote = secp256k1::PublicKey, derive(Hash, PartialEq, Eq))] -pub struct PublicKey( - #[rkyv(getter = public_key_getter, with = FFIPublicKey)] secp256k1::ffi::PublicKey, -); - -fn public_key_getter(p: &secp256k1::PublicKey) -> secp256k1::ffi::PublicKey { - unsafe { *p.as_c_ptr().clone() } -} - -impl From for secp256k1::PublicKey { - fn from(value: PublicKey) -> Self { - value.0.into() - } -} - -impl From for PublicKey { - fn from(value: secp256k1::PublicKey) -> Self { - Self(public_key_getter(&value)) - } -} - -#[derive(Copy, Clone, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp256k1::ffi::PublicKey, derive(Hash, PartialEq, Eq))] -pub struct FFIPublicKey(#[rkyv(getter = ffi_public_key_getter)] [u8; 64]); - -fn ffi_public_key_getter(p: &secp256k1::ffi::PublicKey) -> [u8; 64] { - p.underlying_bytes() -} - -impl From for secp256k1::ffi::PublicKey { - fn from(value: FFIPublicKey) -> Self { - unsafe { Self::from_array_unchecked(value.0) } - } -} - -impl From for FFIPublicKey { - fn from(value: secp256k1::ffi::PublicKey) -> Self { - Self(value.underlying_bytes()) - } -} - -#[derive(Copy, Clone, Debug, Archive, Serialize, Deserialize)] -#[rkyv(remote = subtle::Choice)] -pub struct Choice(#[rkyv(getter = choice_getter)] u8); - -fn choice_getter(c: &subtle::Choice) -> u8 { - c.unwrap_u8() -} - -impl From for subtle::Choice { - fn from(value: Choice) -> Self { - Self::from(value.0) - } -} - -impl From for Choice { - fn from(value: subtle::Choice) -> Self { - Self(value.unwrap_u8()) - } -} - -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Archive, Serialize, Deserialize, -)] -#[rkyv(remote = secp::MaybePoint)] -pub enum MaybePoint { - Infinity, - Valid(#[rkyv(with = Point)] secp::Point), -} - -impl From for secp::MaybePoint { - fn from(value: MaybePoint) -> Self { - match value { - MaybePoint::Infinity => Self::Infinity, - MaybePoint::Valid(p) => Self::Valid(p), - } - } -} - -impl From for MaybePoint { - fn from(value: secp::MaybePoint) -> Self { - match value { - secp::MaybePoint::Infinity => Self::Infinity, - secp::MaybePoint::Valid(p) => Self::Valid(p.into()), - } - } -} - -#[derive(Debug, Copy, Clone, Eq, PartialEq, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp::MaybeScalar)] -pub enum MaybeScalar { - Zero, - Valid(#[rkyv(with = Scalar)] secp::Scalar), -} - -impl From for secp::MaybeScalar { - fn from(value: MaybeScalar) -> Self { - match value { - MaybeScalar::Zero => Self::Zero, - MaybeScalar::Valid(s) => Self::Valid(s), - } - } -} - -impl From for MaybeScalar { - fn from(value: secp::MaybeScalar) -> Self { - match value { - secp::MaybeScalar::Zero => Self::Zero, - secp::MaybeScalar::Valid(s) => Self::Valid(s), - } - } -} - -#[derive(Copy, Clone, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp::Scalar)] -pub struct Scalar { - #[rkyv(with = SecretKey, getter = scalar_getter)] - inner: secp256k1::SecretKey, -} - -fn scalar_getter(s: &secp::Scalar) -> secp256k1::SecretKey { - secp256k1::SecretKey::from_slice(&s.serialize()).unwrap() -} - -impl From for secp::Scalar { - fn from(value: Scalar) -> Self { - Self::from(value.inner) - } -} - -impl From for Scalar { - fn from(value: secp::Scalar) -> Self { - Self { - inner: value.into(), - } - } -} - -#[derive(Copy, Clone, Archive, Serialize, Deserialize)] -#[rkyv(remote = secp256k1::SecretKey)] -pub struct SecretKey( - #[rkyv(getter = secret_key_getter)] [u8; secp256k1::constants::SECRET_KEY_SIZE], -); - -fn secret_key_getter(sk: &secp256k1::SecretKey) -> [u8; secp256k1::constants::SECRET_KEY_SIZE] { - sk.secret_bytes() -} - -impl From for secp256k1::SecretKey { - fn from(value: SecretKey) -> Self { - Self::from_slice(&value.0).unwrap() - } -} - -impl From for SecretKey { - fn from(value: secp256k1::SecretKey) -> Self { - Self(value.secret_bytes()) - } -} diff --git a/crates/musig2/src/rounds.rs b/crates/musig2/src/rounds.rs deleted file mode 100644 index 95701684..00000000 --- a/crates/musig2/src/rounds.rs +++ /dev/null @@ -1,724 +0,0 @@ -use rkyv::{with::Map, Archive, Deserialize, Serialize}; -use secp::{MaybePoint, MaybeScalar, Point, Scalar}; - -use crate::{ - errors::{RoundContributionError, RoundFinalizeError, SignerIndexError, SigningError}, - rkyv_wrappers::{self}, - sign_partial, AdaptorSignature, AggNonce, KeyAggContext, LiftedSignature, NonceSeed, - PartialSignature, PubNonce, SecNonce, SecNonceSpices, -}; - -/// A simple state-machine which receives values of a given type `T` and -/// stores them in a vector at given indices. Returns an error if attempting -/// to fill a slot which is already taken by a different (not-equal) value. -#[derive(Archive, Serialize, Deserialize)] -pub struct Slots { - slots: Vec>, - open_slots: Vec, -} - -impl Slots { - /// Create a new set of slots. - fn new(expected_size: usize) -> Slots { - let mut slots = Vec::new(); - slots.resize(expected_size, None); - let open_slots = Vec::from_iter(0..expected_size); - Slots { slots, open_slots } - } - - /// Add an item to a specific slot, returning an error if the - /// slot is already taken by a different item. Idempotent. - fn place(&mut self, value: T, index: usize) -> Result<(), RoundContributionError> { - if index >= self.slots.len() { - return Err(RoundContributionError::out_of_range( - index, - self.slots.len(), - )); - } - - // Support idempotence. Callers can place the same value into the same - // slot index, which should be a no-op. - if let Some(ref existing) = self.slots[index] { - if &value == existing { - return Ok(()); - } else { - return Err(RoundContributionError::inconsistent_contribution(index)); - } - } - - self.slots[index] = Some(value); - self.open_slots - .remove(self.open_slots.binary_search(&index).unwrap()); - Ok(()) - } - - /// Returns a slice listing all remaining open slots. - fn remaining(&self) -> &[usize] { - self.open_slots.as_ref() - } - - /// Returns the full array of slot values in order. - /// Returns `None` if any slot is not yet filled. - fn finalize(self) -> Result, RoundFinalizeError> { - self.slots - .into_iter() - .map(|opt| opt.ok_or(RoundFinalizeError::Incomplete)) - .collect() - } -} - -#[derive(Archive, Serialize, Deserialize)] -#[rkyv(remote = Slots)] -struct PartialSignatureSlots { - #[rkyv(with = Map>)] - slots: Vec>, - open_slots: Vec, -} - -impl From for Slots { - fn from(partial_signature_slots: PartialSignatureSlots) -> Self { - let PartialSignatureSlots { slots, open_slots } = partial_signature_slots; - Slots { slots, open_slots } - } -} - -impl From> for PartialSignatureSlots { - fn from(partial_signature_slots: Slots) -> Self { - let Slots { slots, open_slots } = partial_signature_slots; - PartialSignatureSlots { slots, open_slots } - } -} - -/// A state machine which manages the first round of a MuSig2 signing session. -/// -/// Its task is to collect [`PubNonce`]s one by one until all signers have provided -/// one, at which point a partial signature can be created on a message using an -/// internally cached [`SecNonce`]. -/// -/// By preventing cloning or copying, and by consuming itself after creating a -/// partial signature, `FirstRound`'s API is written to encourage that a -/// [`SecNonce`] should **never be reused.** Take care not to shoot yourself in -/// the foot by attempting to work around this restriction. -#[derive(Archive, Serialize, Deserialize)] -pub struct FirstRound { - key_agg_ctx: KeyAggContext, - signer_index: usize, // Our key's index in `key_agg_ctx` - secnonce: SecNonce, // Our secret nonce. - pubnonce_slots: Slots, -} - -impl FirstRound { - /// Start the first round of a MuSig2 signing session. - /// - /// Generates the nonce using the given random seed value, which can - /// be any type that converts to `NonceSeed`. Usually this would - /// either be a `[u8; 32]` or any type that implements [`rand::RngCore`] - /// and [`rand::CryptoRng`], such as [`rand::rngs::OsRng`]. - /// If a static byte array is used as the seed, it should be generated - /// using a cryptographically secure RNG and discarded after the `FirstRound` - /// is created. Prefer using a [`rand::CryptoRng`] if possible, so that - /// there is no possibility of reusing the same nonce seed in a new signing - /// session. - /// - /// Returns an error if the given signer index is out of range. - pub fn new( - key_agg_ctx: KeyAggContext, - nonce_seed: impl Into, - signer_index: usize, - spices: SecNonceSpices<'_>, - ) -> Result { - let signer_pubkey: Point = key_agg_ctx - .get_pubkey(signer_index) - .ok_or_else(|| SignerIndexError::new(signer_index, key_agg_ctx.pubkeys().len()))?; - let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); - - let secnonce = SecNonce::build(nonce_seed) - .with_pubkey(signer_pubkey) - .with_aggregated_pubkey(aggregated_pubkey) - .with_extra_input(&(signer_index as u32).to_be_bytes()) - .with_spices(spices) - .build(); - - let pubnonce = secnonce.public_nonce(); - - let mut pubnonce_slots = Slots::new(key_agg_ctx.pubkeys().len()); - pubnonce_slots.place(pubnonce, signer_index).unwrap(); // never fails - - Ok(FirstRound { - key_agg_ctx, - secnonce, - signer_index, - pubnonce_slots, - }) - } - - /// Returns the public nonce which should be shared with other signers. - pub fn our_public_nonce(&self) -> PubNonce { - self.secnonce.public_nonce() - } - - /// Returns a slice of all signer indexes who we have yet to receive a - /// [`PubNonce`] from. Note that since our nonce is generated and cached - /// internally, this slice will never contain the signer index provided to - /// [`FirstRound::new`] - pub fn holdouts(&self) -> &[usize] { - self.pubnonce_slots.remaining() - } - - /// Adds a [`PubNonce`] to the internal state, registering it to a specific - /// signer at a given index. Returns an error if the signer index is out - /// of range, or if we already have a different nonce on-file for that signer. - pub fn receive_nonce( - &mut self, - signer_index: usize, - pubnonce: PubNonce, - ) -> Result<(), RoundContributionError> { - self.pubnonce_slots.place(pubnonce, signer_index) - } - - /// Returns true once all public nonces have been received from every signer. - pub fn is_complete(&self) -> bool { - self.holdouts().is_empty() - } - - /// Finishes the first round once all nonces are received, combining nonces - /// into an aggregated nonce, and creating a partial signature using `seckey` - /// on a given `message`, both of which are stored in the returned `SecondRound`. - /// - /// See [`SecondRound::aggregated_nonce`] to access the aggregated nonce, - /// and [`SecondRound::our_signature`] to access the partial signature. - /// - /// This method intentionally consumes the `FirstRound`, to avoid accidentally - /// reusing a secret-nonce. - /// - /// This method should only be invoked once [`is_complete`][Self::is_complete] - /// returns true, otherwise it will fail. Can also return an error if partial - /// signing fails, probably because the wrong secret key was given. - /// - /// For all partial signatures to be valid, everyone must naturally be signing the - /// same message. - /// - /// This method is effectively the same as invoking - /// [`finalize_adaptor`][Self::finalize_adaptor], but passing [`MaybePoint::Infinity`] - /// as the adaptor point. - pub fn finalize( - self, - seckey: impl Into, - message: M, - ) -> Result, RoundFinalizeError> - where - M: AsRef<[u8]>, - { - self.finalize_adaptor(seckey, MaybePoint::Infinity, message) - } - - /// Finishes the first round once all nonces are received, combining nonces - /// into an aggregated nonce, and creating a partial adaptor signature using - /// `seckey` on a given `message`, both of which are stored in the returned - /// `SecondRound`. - /// - /// The `adaptor_point` is used to verifiably encrypt the partial signature, so that - /// the final aggregated signature will need to be adapted with the discrete log - /// of `adaptor_point` before the signature can be considered valid. All signers - /// must agree on and use the same adaptor point for the final signature to be valid. - /// - /// See [`SecondRound::aggregated_nonce`] to access the aggregated nonce, - /// and [`SecondRound::our_signature`] to access the partial signature. - /// - /// This method intentionally consumes the `FirstRound`, to avoid accidentally - /// reusing a secret-nonce. - /// - /// This method should only be invoked once [`is_complete`][Self::is_complete] - /// returns true, otherwise it will fail. Can also return an error if partial - /// signing fails, probably because the wrong secret key was given. - /// - /// For all partial signatures to be valid, everyone must naturally be signing the - /// same message. - pub fn finalize_adaptor( - self, - seckey: impl Into, - adaptor_point: impl Into, - message: M, - ) -> Result, RoundFinalizeError> - where - M: AsRef<[u8]>, - { - let adaptor_point: MaybePoint = adaptor_point.into(); - let pubnonces: Vec = self.pubnonce_slots.finalize()?; - let aggnonce = pubnonces.iter().sum(); - - let partial_signature = crate::adaptor::sign_partial( - &self.key_agg_ctx, - seckey, - self.secnonce, - &aggnonce, - adaptor_point, - &message, - )?; - - let mut partial_signature_slots = Slots::new(pubnonces.len()); - partial_signature_slots - .place(partial_signature, self.signer_index) - .unwrap(); // never fails - - let second_round = SecondRound { - key_agg_ctx: self.key_agg_ctx, - signer_index: self.signer_index, - pubnonces, - aggnonce, - adaptor_point, - message, - partial_signature_slots, - }; - - Ok(second_round) - } - - /// As an alternative to collecting nonces and partial signatures one-by-one from - /// everyone in the group, signers can opt instead to nominate an _aggregator node_ - /// whose duty is to collect nonces and signatures from all other signers, and - /// then broadcast the aggregated signature once they receive all partial signatures. - /// Doing this dramatically decreases the number of network round-trips required - /// for large groups of signers, and doesn't require any trust in the aggregator node - /// beyond the possibility that they may refuse to reveal the final signature. - /// - /// To use this API with a single aggregator node: - /// - /// - Instantiate the `FirstRound`. - /// - Send the output of [`FirstRound::our_public_nonce`] to the aggregator. - /// - The aggregator node should reply with an [`AggNonce`]. - /// - Once you receive the aggregated nonce, use [`FirstRound::sign_for_aggregator`] instead of - /// [`finalize`][Self::finalize] to consume the `FirstRound` and return a partial signature. - /// - Send this partial signature to the aggregator. - /// - The aggregator (if they are honest) will reply with the aggregated Schnorr signature, - /// which can be verified with [`verify_single`][crate::verify_single] - /// - /// [See the top-level crate documentation for an example](.#single-aggregator). - /// - /// Invoking this method is essentially the same as invoking - /// [`sign_for_aggregator_adaptor`][Self::sign_for_aggregator_adaptor], - /// but passing [`MaybePoint::Infinity`] as the adaptor point. - pub fn sign_for_aggregator( - self, - seckey: impl Into, - message: impl AsRef<[u8]>, - aggregated_nonce: &AggNonce, - ) -> Result - where - T: From, - { - sign_partial( - &self.key_agg_ctx, - seckey, - self.secnonce, - aggregated_nonce, - &message, - ) - } - - /// As an alternative to collecting nonces and partial signatures one-by-one from - /// everyone in the group, signers can opt instead to nominate an _aggregator node_ - /// whose duty is to collect nonces and signatures from all other signers, and - /// then broadcast the aggregated signature once they receive all partial signatures. - /// Doing this dramatically decreases the number of network round-trips required - /// for large groups of signers, and doesn't require any trust in the aggregator node - /// beyond the possibility that they may refuse to reveal the final signature. - /// - /// To use this API with a single aggregator node: - /// - /// - The group must agree on an `adaptor_point` which will be used to encrypt signatures. - /// - Instantiate the `FirstRound`. - /// - Send the output of [`FirstRound::our_public_nonce`] to the aggregator. - /// - The aggregator node should reply with an [`AggNonce`]. - /// - Once you receive the aggregated nonce, use [`FirstRound::sign_for_aggregator_adaptor`] - /// instead of [`finalize_adaptor`][Self::finalize_adaptor] to consume the `FirstRound` and - /// return a partial signature. - /// - Send this partial signature to the aggregator. - /// - The aggregator (if they are honest) will reply with the aggregated Schnorr signature, - /// which can be verified with [`adaptor::verify_single`][crate::adaptor::verify_single] - /// - /// [See the top-level crate documentation for an example](.#single-aggregator). - pub fn sign_for_aggregator_adaptor( - self, - seckey: impl Into, - adaptor_point: impl Into, - message: impl AsRef<[u8]>, - aggregated_nonce: &AggNonce, - ) -> Result - where - T: From, - { - crate::adaptor::sign_partial( - &self.key_agg_ctx, - seckey, - self.secnonce, - aggregated_nonce, - adaptor_point, - &message, - ) - } -} - -/// A state machine to manage second round of a MuSig2 signing session. -/// -/// This round handles collecting partial signatures one by one. Once -/// all signers have provided a signature, it can be finalized into -/// an aggregated Schnorr signature valid for the group's aggregated key. -#[derive(Archive, Serialize, Deserialize)] -pub struct SecondRound> { - key_agg_ctx: KeyAggContext, - signer_index: usize, - pubnonces: Vec, - aggnonce: AggNonce, - #[rkyv(with = rkyv_wrappers::MaybePoint)] - adaptor_point: MaybePoint, - message: M, - #[rkyv(with = PartialSignatureSlots)] - partial_signature_slots: Slots, -} - -impl> SecondRound { - /// Returns the aggregated nonce built from the nonces provided in the first round. - /// Signers who find themselves in an aggregator role can distribute this aggregated - /// nonce to other signers to that they can produce an aggregated signature without - /// 1:1 communication between every pair of signers. - pub fn aggregated_nonce(&self) -> &AggNonce { - &self.aggnonce - } - - /// Returns the partial signature created during finalization of the first round. - pub fn our_signature>(&self) -> T { - self.partial_signature_slots.slots[self.signer_index] - .map(T::from) - .unwrap() // never fails - } - - /// Returns a slice of all signer indexes from whom we have yet to receive a - /// [`PartialSignature`]. Note that since our signature was constructed - /// at the end of the first round, this slice will never contain the signer - /// index provided to [`FirstRound::new`]. - pub fn holdouts(&self) -> &[usize] { - self.partial_signature_slots.remaining() - } - - /// Adds a [`PartialSignature`] to the internal state, registering it to a specific - /// signer at a given index. Returns an error if the signature is not valid, or if - /// the given signer index is out of range, or if we already have a different partial - /// signature on-file for that signer. - pub fn receive_signature( - &mut self, - signer_index: usize, - partial_signature: impl Into, - ) -> Result<(), RoundContributionError> { - let partial_signature: PartialSignature = partial_signature.into(); - let signer_pubkey: Point = self.key_agg_ctx.get_pubkey(signer_index).ok_or_else(|| { - RoundContributionError::out_of_range(signer_index, self.key_agg_ctx.pubkeys().len()) - })?; - - crate::adaptor::verify_partial( - &self.key_agg_ctx, - partial_signature, - &self.aggnonce, - self.adaptor_point, - signer_pubkey, - &self.pubnonces[signer_index], - &self.message, - ) - .map_err(|_| RoundContributionError::invalid_signature(signer_index))?; - - self.partial_signature_slots - .place(partial_signature, signer_index)?; - - Ok(()) - } - - /// Returns true once we have all partial signatures from the group. - pub fn is_complete(&self) -> bool { - self.holdouts().is_empty() - } - - /// Finishes the second round once all partial signatures are received, - /// combining signatures into an aggregated signature on the `message` - /// given to [`FirstRound::finalize`]. - /// - /// This method should only be invoked once [`is_complete`][Self::is_complete] - /// returns true, otherwise it will fail. Can also return an error if partial - /// signature aggregation fails, but if [`receive_signature`][Self::receive_signature] - /// didn't complain, then finalizing will succeed with overwhelming probability. - /// - /// If the [`FirstRound`] was finalized with [`FirstRound::finalize_adaptor`], then - /// the second round must also be finalized with [`SecondRound::finalize_adaptor`], - /// otherwise this method will return [`RoundFinalizeError::InvalidAggregatedSignature`]. - pub fn finalize(self) -> Result - where - T: From, - { - let sig = self - .finalize_adaptor::()? - .adapt(MaybeScalar::Zero) - .expect("finalizing with empty adaptor should never result in an adaptor failure"); - - Ok(T::from(sig)) - } - - /// Finishes the second round once all partial adaptor signatures are received, - /// combining signatures into an aggregated adaptor signature on the `message` - /// given to [`FirstRound::finalize`]. - /// - /// To make this signature valid, it must then be adapted with the discrete log - /// of the adaptor point given to [`FirstRound::finalize`]. - /// - /// This method should only be invoked once [`is_complete`][Self::is_complete] - /// returns true, otherwise it will fail. Can also return an error if partial - /// signature aggregation fails, but if [`receive_signature`][Self::receive_signature] - /// didn't complain, then finalizing will succeed with overwhelming probability. - /// - /// If this signing session did not use adaptor signatures, the signature returned by - /// this method will be a valid signature which can be adapted with `MaybeScalar::Zero`. - pub fn finalize_adaptor(self) -> Result { - let partial_signatures: Vec = self.partial_signature_slots.finalize()?; - let final_signature = crate::adaptor::aggregate_partial_signatures( - &self.key_agg_ctx, - &self.aggnonce, - self.adaptor_point, - partial_signatures, - &self.message, - )?; - Ok(final_signature) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{verify_single, LiftedSignature}; - - #[test] - fn test_rounds_api() { - // SETUP phase: key aggregation - let seckeys = [ - "c52be0df73ef4354b2953deb9fdf77749b86946132176a33146f95d46fb065f3" - .parse::() - .unwrap(), - "c731a6d52303c68f3efc6c4262c99269140809c39f651196d7264d225c25360d" - .parse::() - .unwrap(), - "10e7721a3aa6de7a98cecdbd7c706c836a907ca46a43235a7b498b12498f98f0" - .parse::() - .unwrap(), - ]; - - let pubkeys = seckeys.iter().map(|sk| sk.base_point_mul()); - let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); - - // ROUND 1: nonces - - let message = "hello interwebz!"; - - let mut first_rounds: Vec = seckeys - .iter() - .enumerate() - .map(|(i, &sk)| { - FirstRound::new( - key_agg_ctx.clone(), - [0xAC; 32], - i, - SecNonceSpices::new().with_seckey(sk).with_message(&message), - ) - .unwrap_or_else(|_| { - panic!("failed to construct FirstRound machine for signer {}", i) - }) - }) - .collect(); - - // Nobody's round should be complete right after it was created. - for (i, round) in first_rounds.iter().enumerate() { - assert!( - !round.is_complete(), - "round should not be complete without any nonces" - ); - - let mut expected_holdouts: Vec = (0..seckeys.len()).collect(); - expected_holdouts.remove(i); - assert_eq!( - round.holdouts(), - expected_holdouts, - "expected holdouts list to contain all other signers" - ) - } - - let pubnonces: Vec = first_rounds - .iter() - .map(|first_round| first_round.our_public_nonce()) - .collect(); - - // Distribute the pubnonces. - for (i, nonce) in pubnonces.iter().enumerate() { - for round in first_rounds.iter_mut() { - round - .receive_nonce(i, nonce.clone()) - .unwrap_or_else(|_| panic!("should receive nonce {} OK", i)); - - let mut expected_holdouts: Vec = (0..seckeys.len()).collect(); - expected_holdouts.retain(|&j| j != round.signer_index && j > i); - assert_eq!(round.holdouts(), expected_holdouts); - - // Confirm the round completes only once all nonces are received - if expected_holdouts.is_empty() { - assert!( - round.is_complete(), - "first round should have completed after signer {} receiving nonce {}", - round.signer_index, - i - ); - } else { - assert!( - !round.is_complete(), - "first round should not have completed after signer {} receiving nonce {}", - round.signer_index, - i - ); - } - } - } - - // The first round of nonce sharing should be complete now. - for round in first_rounds.iter() { - assert!(round.is_complete()); - } - - assert_eq!( - first_rounds[0].receive_nonce(2, pubnonces[1].clone()), - Err(RoundContributionError::inconsistent_contribution(2)), - "receiving a different nonce at a previously used index should fail" - ); - assert_eq!( - first_rounds[0].receive_nonce(pubnonces.len() + 1, pubnonces[1].clone()), - Err(RoundContributionError::out_of_range( - pubnonces.len() + 1, - pubnonces.len() - )), - "receiving a nonce at an invalid index should fail" - ); - - // ROUND 2: signing - - let mut second_rounds: Vec> = first_rounds - .into_iter() - .enumerate() - .map(|(i, first_round)| -> SecondRound<&str> { - first_round - .finalize(seckeys[i], message) - .unwrap_or_else(|_| panic!("failed to finalize first round for signer {}", i)) - }) - .collect(); - - for round in second_rounds.iter() { - assert!( - !round.is_complete(), - "second round should not be complete yet" - ); - } - - // Invalid partial signatures should be automatically rejected. - { - let wrong_nonce = SecNonce::build([0xCC; 32]).build(); - let invalid_partial_signature: PartialSignature = sign_partial( - &key_agg_ctx, - seckeys[0], - wrong_nonce, - &second_rounds[0].aggnonce, - message, - ) - .unwrap(); - - assert_eq!( - second_rounds[1].receive_signature(0, invalid_partial_signature), - Err(RoundContributionError::invalid_signature(0)), - "partial signature with invalid nonce should be rejected" - ); - } - - let partial_signatures: Vec = second_rounds - .iter() - .map(|round| round.our_signature()) - .collect(); - - // Distribute the partial signatures. - for (i, &partial_signature) in partial_signatures.iter().enumerate() { - for (receiver_index, round) in second_rounds.iter_mut().enumerate() { - round - .receive_signature(i, partial_signature) - .unwrap_or_else(|_| panic!("should receive partial signature {} OK", i)); - - let mut expected_holdouts: Vec = (0..seckeys.len()).collect(); - expected_holdouts.retain(|&j| j != receiver_index && j > i); - assert_eq!(round.holdouts(), expected_holdouts); - - // Confirm the round completes only once all signatures are received - if expected_holdouts.is_empty() { - assert!( - round.is_complete(), - "second round should have completed after signer {} receiving partial signature {}", - receiver_index, - i - ); - } else { - assert!( - !round.is_complete(), - "second round should not have completed after signer {} receiving partial signature {}", - receiver_index, - i - ); - } - } - } - - // The second round should be complete now that everyone has each - // other's partial signatures. - for round in second_rounds.iter() { - assert!(round.is_complete()); - } - - // Test supplying signatures at wrong indices - assert_eq!( - second_rounds[0].receive_signature(2, partial_signatures[1]), - Err(RoundContributionError::invalid_signature(2)), - "receiving a valid partial signature for the wrong signer should fail" - ); - assert_eq!( - second_rounds[0].receive_signature(partial_signatures.len() + 1, partial_signatures[1]), - Err(RoundContributionError::out_of_range( - partial_signatures.len() + 1, - partial_signatures.len() - )), - "receiving a partial signature at an invalid index should fail" - ); - - // FINALIZATION: signatures can now be aggregated. - let mut signatures: Vec = second_rounds - .into_iter() - .enumerate() - .map(|(i, round)| { - round - .finalize() - .unwrap_or_else(|_| panic!("failed to finalize second round for signer {}", i)) - }) - .collect(); - - let last_sig = signatures.pop().unwrap(); - - // All signers should output the same aggregated signature. - for sig in signatures { - assert_eq!( - sig, last_sig, - "some signers created different aggregated signatures" - ); - } - - // and of course, the sig should be verifiable as a standard schnorr signature. - let aggregated_pubkey: Point = key_agg_ctx.aggregated_pubkey(); - verify_single(aggregated_pubkey, last_sig, message) - .expect("aggregated signature should be valid"); - } -} diff --git a/crates/musig2/src/sig_agg.rs b/crates/musig2/src/sig_agg.rs deleted file mode 100644 index 1d4d1462..00000000 --- a/crates/musig2/src/sig_agg.rs +++ /dev/null @@ -1,283 +0,0 @@ -use secp::{MaybePoint, MaybeScalar, Point, G}; - -use crate::errors::VerifyError; -use crate::{ - compute_challenge_hash_tweak, AdaptorSignature, AggNonce, KeyAggContext, LiftedSignature, - PartialSignature, -}; - -/// Aggregate a collection of partial adaptor signatures together into a final -/// adaptor signature on a given `message`, under the aggregated public key in -/// `key_agg_ctx`. -/// -/// The resulting signature will not be valid unless adapted with the discrete log -/// of the `adaptor_point`. -/// -/// Returns an error if the resulting signature would not be valid. -pub fn aggregate_partial_adaptor_signatures>( - key_agg_ctx: &KeyAggContext, - aggregated_nonce: &AggNonce, - adaptor_point: impl Into, - partial_signatures: impl IntoIterator, - message: impl AsRef<[u8]>, -) -> Result { - let adaptor_point: MaybePoint = adaptor_point.into(); - let aggregated_pubkey = key_agg_ctx.pubkey; - - let b: MaybeScalar = aggregated_nonce.nonce_coefficient(aggregated_pubkey, &message); - let final_nonce: Point = aggregated_nonce.final_nonce(b); - let adapted_nonce = final_nonce + adaptor_point; - let nonce_x_bytes = adapted_nonce.serialize_xonly(); - let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &aggregated_pubkey, &message); - - let aggregated_signature = partial_signatures - .into_iter() - .map(|sig| sig.into()) - .sum::() - + (e * key_agg_ctx.tweak_acc).negate_if(aggregated_pubkey.parity()); - - let effective_nonce = if adapted_nonce.has_even_y() { - final_nonce - } else { - -final_nonce - }; - - // Ensure the signature will verify as valid. - if aggregated_signature * G != effective_nonce + e * aggregated_pubkey.to_even_y() { - return Err(VerifyError::BadSignature); - } - - let adaptor_sig = AdaptorSignature { - R: MaybePoint::Valid(final_nonce), - s: aggregated_signature, - }; - Ok(adaptor_sig) -} - -/// Aggregate a collection of partial signatures together into a final -/// signature on a given `message`, valid under the aggregated public -/// key in `key_agg_ctx`. -/// -/// Returns an error if the resulting signature would not be valid. -pub fn aggregate_partial_signatures( - key_agg_ctx: &KeyAggContext, - aggregated_nonce: &AggNonce, - partial_signatures: impl IntoIterator, - message: impl AsRef<[u8]>, -) -> Result -where - S: Into, - T: From, -{ - let sig = aggregate_partial_adaptor_signatures( - key_agg_ctx, - aggregated_nonce, - MaybePoint::Infinity, - partial_signatures, - message, - )? - .adapt(MaybeScalar::Zero) - .map(T::from) - .expect("aggregating with empty adaptor should never result in an adaptor failure"); - - Ok(sig) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::testhex; - use crate::{verify_single, CompactSignature, PubNonce, SecNonce}; - - use secp::{Point, Scalar}; - - #[test] - fn test_aggregate_partial_signatures() { - const SIG_AGG_VECTORS: &[u8] = include_bytes!("test_vectors/sig_agg_vectors.json"); - - #[derive(serde::Deserialize)] - struct ValidSigAggTestCase { - #[serde(rename = "aggnonce")] - aggregated_nonce: AggNonce, - nonce_indices: Vec, - key_indices: Vec, - tweak_indices: Vec, - is_xonly: Vec, - psig_indices: Vec, - - #[serde(rename = "expected")] - aggregated_signature: CompactSignature, - } - - #[derive(serde::Deserialize)] - struct SigAggVectors { - pubkeys: Vec, - - #[serde(rename = "pnonces")] - public_nonces: Vec, - - tweaks: Vec, - - #[serde(rename = "psigs", deserialize_with = "testhex::deserialize_vec")] - partial_signatures: Vec>, - - #[serde(rename = "msg", deserialize_with = "testhex::deserialize")] - message: Vec, - - valid_test_cases: Vec, - } - - let vectors: SigAggVectors = serde_json::from_slice(SIG_AGG_VECTORS) - .expect("failed to parse test vectors from sig_agg_vectors.json"); - - for test_case in vectors.valid_test_cases { - let pubkeys = test_case - .key_indices - .into_iter() - .map(|i| vectors.pubkeys[i]); - - let public_nonces = test_case - .nonce_indices - .into_iter() - .map(|i| &vectors.public_nonces[i]); - - let aggregated_nonce = AggNonce::sum(public_nonces); - - assert_eq!( - &aggregated_nonce, &test_case.aggregated_nonce, - "aggregated nonce does not match test vector" - ); - - let mut key_agg_ctx = - KeyAggContext::new(pubkeys).expect("error constructing key aggregation context"); - - key_agg_ctx = test_case - .tweak_indices - .into_iter() - .map(|i| vectors.tweaks[i]) - .zip(test_case.is_xonly) - .fold(key_agg_ctx, |ctx, (tweak, is_xonly)| { - ctx.with_tweak(tweak, is_xonly).unwrap_or_else(|_| { - panic!("failed to tweak key agg context with {:x}", tweak) - }) - }); - - let partial_signatures: Vec = test_case - .psig_indices - .into_iter() - .map(|i| { - Scalar::try_from(vectors.partial_signatures[i].as_slice()) - .expect("failed to parse partial signature") - }) - .collect(); - - let aggregated_signature: CompactSignature = aggregate_partial_signatures( - &key_agg_ctx, - &aggregated_nonce, - partial_signatures, - &vectors.message, - ) - .expect("failed to aggregate partial signatures"); - - assert_eq!( - &aggregated_signature, &test_case.aggregated_signature, - "incorrect aggregated signature" - ); - - verify_single(key_agg_ctx.pubkey, aggregated_signature, &vectors.message) - .unwrap_or_else(|_| { - panic!( - "aggregated signature {} should be valid BIP340 signature", - aggregated_signature - ) - }); - } - } - - #[test] - fn test_adaptor_signature_aggregation() { - const ITERATIONS: usize = 10; - - for _ in 0..ITERATIONS { - let seckeys = [ - Scalar::random(&mut rand::thread_rng()), - Scalar::random(&mut rand::thread_rng()), - Scalar::random(&mut rand::thread_rng()), - ]; - - let pubkeys = [ - seckeys[0].base_point_mul(), - seckeys[1].base_point_mul(), - seckeys[2].base_point_mul(), - ]; - - let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); - - let message = b"danger, will robinson!"; - - let secnonces = [ - SecNonce::random(&mut rand::thread_rng()), - SecNonce::random(&mut rand::thread_rng()), - SecNonce::random(&mut rand::thread_rng()), - ]; - - let pubnonces = [ - secnonces[0].public_nonce(), - secnonces[1].public_nonce(), - secnonces[2].public_nonce(), - ]; - - let aggnonce = AggNonce::sum(&pubnonces); - - let adaptor_secret = Scalar::random(&mut rand::thread_rng()); - let adaptor_point = adaptor_secret.base_point_mul(); - - let partial_signatures: Vec = seckeys - .into_iter() - .zip(secnonces) - .map(|(seckey, secnonce)| { - crate::adaptor::sign_partial( - &key_agg_ctx, - seckey, - secnonce, - &aggnonce, - adaptor_point, - message, - ) - }) - .collect::, _>>() - .expect("failed to create partial adaptor signatures"); - - let adaptor_signature: AdaptorSignature = crate::adaptor::aggregate_partial_signatures( - &key_agg_ctx, - &aggnonce, - adaptor_point, - partial_signatures.iter().copied(), - message, - ) - .expect("failed to aggregate partial adaptor signatures"); - - crate::adaptor::verify_single( - key_agg_ctx.aggregated_pubkey::(), - &adaptor_signature, - message, - adaptor_point, - ) - .expect("invalid aggregated adaptor signature"); - - let valid_signature = adaptor_signature.adapt(adaptor_secret).unwrap(); - verify_single( - key_agg_ctx.aggregated_pubkey::(), - valid_signature, - message, - ) - .expect("invalid decrypted adaptor signature"); - - let revealed: MaybeScalar = adaptor_signature - .reveal_secret(&valid_signature) - .expect("should compute adaptor secret from decrypted signature"); - - assert_eq!(revealed, MaybeScalar::Valid(adaptor_secret)); - } - } -} diff --git a/crates/musig2/src/signature.rs b/crates/musig2/src/signature.rs deleted file mode 100644 index 075b0fcc..00000000 --- a/crates/musig2/src/signature.rs +++ /dev/null @@ -1,432 +0,0 @@ -use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; - -use crate::errors::DecodeError; -use crate::BinaryEncoding; - -/// The number of bytes in a binary-serialized Schnorr signature. -pub const SCHNORR_SIGNATURE_SIZE: usize = 64; - -/// Represents a compacted Schnorr signature, either -/// from an aggregated signing session or a single signer. -/// -/// It differs from [`LiftedSignature`] in that a `CompactSignature` -/// contains the X-only serialized coordinate of the signature's nonce -/// point `R`, whereas a [`LiftedSignature`] contains the parsed curve -/// point `R`. -/// -/// Parsing a curve point from a byte array requires some computations which -/// can be optimized away during verification. This is why `CompactSignature` -/// is its own separate type. -/// -/// Rules for when to use each signature type during verification: -/// -/// - Prefer using [`CompactSignature`] when parsing and verifying single -/// signatures. That will produce faster results as you won't need to -/// lift the X-only coordinate of the nonce-point to verify the signature. -/// - Prefer using [`LiftedSignature`] when using batch verification, -/// because lifted signatures are required for batch verification -/// so you might as well keep the signatures in lifted form. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CompactSignature { - /// The X-only byte representation of the public nonce point `R`. - pub rx: [u8; 32], - - /// The signature scalar which proves knowledge of the secret key and nonce. - pub s: MaybeScalar, -} - -impl CompactSignature { - /// Constructs a `CompactSignature` from a signature pair `(R, s)`. - pub fn new(R: impl Into, s: impl Into) -> CompactSignature { - CompactSignature { - rx: R.into().serialize_xonly(), - s: s.into(), - } - } - - /// Lifts the nonce point X coordinate to a proper point with even parity, - /// returning an error if the coordinate was not on the curve. - pub fn lift_nonce(&self) -> Result { - let R = Point::lift_x(&self.rx)?; - Ok(LiftedSignature { R, s: self.s }) - } -} - -/// A representation of a Schnorr signature point+scalar pair `(R, s)`. -/// -/// Differs from [`CompactSignature`] in that a `LiftedSignature` -/// contains the full nonce point `R`, which is parsed as a valid -/// curve point. -/// -/// Rules for when to use each signature type during verification: -/// -/// - Prefer using [`CompactSignature`] when parsing and verifying single -/// signatures. That will produce faster results as you won't need to -/// lift the X-only coordinate of the nonce-point to verify the signature. -/// - Prefer using [`LiftedSignature`] when using batch verification, -/// because lifted signatures are required for batch verification -/// so you might as well keep the signatures in lifted form. -/// -/// A `LiftedSignature` has the exact sime binary serialization -/// format as a [`CompactSignature`], because the Y-coordinate -/// of the nonce point is implicit - It is always assumed to be -/// the even-parity point. -/// -/// To construct a `LiftedSignature`, use [`LiftedSignature::new`] -/// to ensure the Y-coordinate of the nonce point is always converted -/// to even-parity. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LiftedSignature { - pub(crate) R: Point, - pub(crate) s: MaybeScalar, -} - -impl LiftedSignature { - /// Constructs a new lifted signature by converting the nonce point `R` - /// to even parity. - /// - /// Accepts any types which convert to a [`secp::Point`] and - /// [`secp::MaybeScalar`]. - pub fn new(R: impl Into, s: impl Into) -> LiftedSignature { - LiftedSignature { - R: R.into().to_even_y(), - s: s.into(), - } - } - - /// Compact the finalized signature by serializing the - /// nonce point as an X-coordinate-only byte array. - pub fn compact(&self) -> CompactSignature { - CompactSignature::new(self.R, self.s) - } - - /// Encrypts an existing valid signature by subtracting a given adaptor secret. - pub fn encrypt(&self, adaptor_secret: impl Into) -> AdaptorSignature { - AdaptorSignature::new(self.R, self.s).encrypt(adaptor_secret) - } - - /// Unzip this signature pair into a tuple of any two types - /// which convert from [`secp::Point`] and [`secp::MaybeScalar`]. - /// - /// ``` - /// // This allows us to use `R` as a variable name. - /// #![allow(non_snake_case)] - /// - /// let signature = "c1de0db357c5d780c69624d0ab266a3b6866301adc85b66cc9fce26d2a60b72c\ - /// 659c15ed9bc81df681e1e0607cf44cc08e77396f74359de1e6e6ff365ca94dae" - /// .parse::() - /// .unwrap(); - /// - /// let (R, s): ([u8; 33], [u8; 32]) = signature.unzip(); - /// let (R, s): (secp::Point, secp::MaybeScalar) = signature.unzip(); - /// # #[cfg(feature = "k256")] - /// # { - /// let (R, s): (k256::PublicKey, k256::Scalar) = signature.unzip(); - /// # } - /// # #[cfg(feature = "secp256k1")] - /// # { - /// let (R, s): (secp256k1::PublicKey, secp::MaybeScalar) = signature.unzip(); - /// # } - /// ``` - pub fn unzip(&self) -> (P, S) - where - P: From, - S: From, - { - (P::from(self.R), S::from(self.s)) - } -} - -/// A representation of a Schnorr adaptor signature point+scalar pair `(R', s')`. -/// -/// Differs from [`LiftedSignature`] in that an `AdaptorSignature` is explicitly -/// modified with by specific scalar offset called the _adaptor secret,_ so that -/// only by learning the adaptor secret can its holder convert it -/// into a valid BIP340 signature. -/// -/// Since `AdaptorSignature` is not meant for on-chain consensus, the nonce -/// point `R` can have either even or odd parity, and so `AdaptorSignature` -/// is encoded as a 65 byte array which includes the compressed `R` point. -/// -/// To learn more about adaptor signatures and how to use them, see the docs -/// in [the adaptor module][crate::adaptor]. -/// -/// To construct an `AdaptorSignature`, use [`LiftedSignature::encrypt`], -/// [`adaptor::sign_solo`][crate::adaptor::sign_solo], or -/// [`adaptor::aggregate_partial_signatures`][crate::adaptor::aggregate_partial_signatures]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct AdaptorSignature { - pub(crate) R: MaybePoint, - pub(crate) s: MaybeScalar, -} - -impl AdaptorSignature { - /// Constructs a new adaptor signature from a nonce and scalar pair. - /// - /// Accepts any types which convert to a [`secp::MaybePoint`] and - /// [`secp::MaybeScalar`]. - pub fn new(R: impl Into, s: impl Into) -> AdaptorSignature { - AdaptorSignature { - R: R.into(), - s: s.into(), - } - } - - /// Adapts the signature into a lifted signature with a given adaptor secret. - /// - /// Returns `None` if the nonce resulting from adding the adaptor point is the - /// point at infinity. - /// - /// The resulting signature is not guaranteed to be valid unless the - ///`AdaptorSignature` was already verified with - /// [`adaptor::verify_single`][crate::adaptor::verify_single]. - /// If not, make sure to verify the resulting lifted signature - /// using [`verify_single`][crate::verify_single]. - pub fn adapt>( - &self, - adaptor_secret: impl Into, - ) -> Option { - let adaptor_secret: MaybeScalar = adaptor_secret.into(); - let adapted_nonce = (self.R + adaptor_secret * G).into_option()?; - let adapted_sig = self.s + adaptor_secret.negate_if(adapted_nonce.parity()); - Some(T::from(LiftedSignature::new(adapted_nonce, adapted_sig))) - } - - /// Encrypts an existing adaptor signature again, by subtracting another adaptor secret. - pub fn encrypt(&self, adaptor_secret: impl Into) -> AdaptorSignature { - let adaptor_secret: Scalar = adaptor_secret.into(); - AdaptorSignature { - R: self.R - adaptor_secret * G, - s: self.s - adaptor_secret, - } - } - - /// Using a decrypted signature `final_sig`, this method computes the - /// adaptor secret used to encrypt this signature. - /// - /// Returns `None` if `final_sig` is not related to this adaptor signature. - pub fn reveal_secret(&self, final_sig: &LiftedSignature) -> Option - where - S: From, - { - let t = final_sig.s - self.s; - let T = t * G; - - if T == final_sig.R - self.R { - Some(S::from(t)) - } else if T == final_sig.R + self.R { - Some(S::from(-t)) - } else { - None - } - } - - /// Unzip this signature pair into a tuple of any two types - /// which convert from [`secp::MaybePoint`] and [`secp::MaybeScalar`]. - /// - /// ``` - /// // This allows us to use `R` as a variable name. - /// #![allow(non_snake_case)] - /// - /// let signature = "02c1de0db357c5d780c69624d0ab266a3b6866301adc85b66cc9fce26d2a60b72c\ - /// 659c15ed9bc81df681e1e0607cf44cc08e77396f74359de1e6e6ff365ca94dae" - /// .parse::() - /// .unwrap(); - /// - /// let (R, s): ([u8; 33], [u8; 32]) = signature.unzip(); - /// let (R, s): (secp::MaybePoint, secp::MaybeScalar) = signature.unzip(); - /// # #[cfg(feature = "k256")] - /// # { - /// let (R, s): (k256::AffinePoint, k256::Scalar) = signature.unzip(); - /// # } - /// ``` - pub fn unzip(&self) -> (P, S) - where - P: From, - S: From, - { - (P::from(self.R), S::from(self.s)) - } -} - -mod encodings { - use super::*; - - impl BinaryEncoding for CompactSignature { - type Serialized = [u8; SCHNORR_SIGNATURE_SIZE]; - - /// Serializes the signature to a compact 64-byte encoding, - /// including the X coordinate of the `R` point and the - /// serialized `s` scalar. - fn to_bytes(&self) -> Self::Serialized { - let mut serialized = [0u8; SCHNORR_SIGNATURE_SIZE]; - serialized[..32].clone_from_slice(&self.rx); - serialized[32..].clone_from_slice(&self.s.serialize()); - serialized - } - - /// Deserialize a compact Schnorr signature from a byte slice. This - /// slice must be exactly [`SCHNORR_SIGNATURE_SIZE`] bytes long. - fn from_bytes(signature_bytes: &[u8]) -> Result> { - if signature_bytes.len() != SCHNORR_SIGNATURE_SIZE { - return Err(DecodeError::bad_length(signature_bytes.len())); - } - let rx = <[u8; 32]>::try_from(&signature_bytes[..32]).unwrap(); - let s = MaybeScalar::try_from(&signature_bytes[32..])?; - Ok(CompactSignature { rx, s }) - } - } - - impl BinaryEncoding for LiftedSignature { - type Serialized = [u8; SCHNORR_SIGNATURE_SIZE]; - - /// Serializes the signature to a compact 64-byte encoding, - /// including the X coordinate of the `R` point and the - /// serialized `s` scalar. - fn to_bytes(&self) -> Self::Serialized { - CompactSignature::from(*self).to_bytes() - } - - /// Deserialize a compact Schnorr signature from a byte slice. This - /// slice must be exactly [`SCHNORR_SIGNATURE_SIZE`] bytes long. - fn from_bytes(bytes: &[u8]) -> Result> { - let compact_signature = CompactSignature::from_bytes(bytes).map_err(|e| e.convert())?; - Ok(compact_signature.lift_nonce()?) - } - } - - impl BinaryEncoding for AdaptorSignature { - type Serialized = [u8; 65]; - - /// Serializes the signature to a compressed 65-byte encoding, - /// including the compressed `R` point and the serialized `s` scalar. - fn to_bytes(&self) -> Self::Serialized { - let mut serialized = [0u8; 65]; - serialized[..33].clone_from_slice(&self.R.serialize()); - serialized[33..].clone_from_slice(&self.s.serialize()); - serialized - } - - /// Deserialize an adaptor signature from a byte slice. This - /// slice must be exactly 65 bytes long. - fn from_bytes(signature_bytes: &[u8]) -> Result> { - if signature_bytes.len() != 65 { - return Err(DecodeError::bad_length(signature_bytes.len())); - } - let R = MaybePoint::try_from(&signature_bytes[..33])?; - let s = MaybeScalar::try_from(&signature_bytes[33..])?; - Ok(AdaptorSignature { R, s }) - } - } - - impl_encoding_traits!(CompactSignature, SCHNORR_SIGNATURE_SIZE); - impl_encoding_traits!(LiftedSignature, SCHNORR_SIGNATURE_SIZE); - impl_encoding_traits!(AdaptorSignature, 65); - - impl_hex_display!(CompactSignature); - impl_hex_display!(LiftedSignature); - impl_hex_display!(AdaptorSignature); -} - -mod internal_conversions { - use super::*; - - impl TryFrom for LiftedSignature { - type Error = secp::errors::InvalidPointBytes; - - /// Convert the compact signature into an `(R, s)` pair by lifting - /// the nonce point's X-coordinate representation. Fails if the - /// X-coordinate bytes do not represent a valid curve point. - fn try_from(signature: CompactSignature) -> Result { - signature.lift_nonce() - } - } - - impl From for CompactSignature { - /// Converts a pair `(R, s)` into a schnorr signature struct. - fn from(signature: LiftedSignature) -> Self { - signature.compact() - } - } -} - -#[cfg(feature = "secp256k1")] -mod secp256k1_conversions { - use super::*; - - impl TryFrom for CompactSignature { - type Error = DecodeError; - fn try_from(signature: secp256k1::schnorr::Signature) -> Result { - Self::try_from(signature.serialize()) - } - } - - impl TryFrom for LiftedSignature { - type Error = DecodeError; - fn try_from(signature: secp256k1::schnorr::Signature) -> Result { - Self::try_from(signature.serialize()) - } - } - - impl From for secp256k1::schnorr::Signature { - fn from(signature: CompactSignature) -> Self { - Self::from_slice(&signature.to_bytes()).unwrap() // Never fails - } - } - - impl From for secp256k1::schnorr::Signature { - fn from(signature: LiftedSignature) -> Self { - Self::from_slice(&signature.to_bytes()).unwrap() // Never fails - } - } -} - -#[cfg(feature = "k256")] -mod k256_conversions { - use super::*; - - impl From<(k256::PublicKey, k256::Scalar)> for CompactSignature { - fn from((R, s): (k256::PublicKey, k256::Scalar)) -> Self { - CompactSignature::new(R, s) - } - } - - impl From<(k256::PublicKey, k256::Scalar)> for LiftedSignature { - fn from((R, s): (k256::PublicKey, k256::Scalar)) -> Self { - LiftedSignature::new(R, s) - } - } - - impl TryFrom for (k256::PublicKey, k256::Scalar) { - type Error = secp::errors::InvalidPointBytes; - fn try_from(signature: CompactSignature) -> Result { - Ok(signature.lift_nonce()?.unzip()) - } - } - - impl From for (k256::PublicKey, k256::Scalar) { - fn from(signature: LiftedSignature) -> Self { - signature.unzip() - } - } - - impl From for (k256::AffinePoint, k256::Scalar) { - fn from(signature: LiftedSignature) -> Self { - signature.unzip() - } - } - - #[cfg(feature = "k256")] - impl From for k256::WideBytes { - fn from(signature: CompactSignature) -> Self { - <[u8; SCHNORR_SIGNATURE_SIZE]>::from(signature).into() - } - } - - #[cfg(feature = "k256")] - impl From for k256::WideBytes { - fn from(signature: LiftedSignature) -> Self { - <[u8; SCHNORR_SIGNATURE_SIZE]>::from(signature).into() - } - } -} diff --git a/crates/musig2/src/signing.rs b/crates/musig2/src/signing.rs deleted file mode 100644 index 60ed94a4..00000000 --- a/crates/musig2/src/signing.rs +++ /dev/null @@ -1,615 +0,0 @@ -use crate::errors::{SigningError, VerifyError}; -use crate::{tagged_hashes, AggNonce, KeyAggContext, PubNonce, SecNonce}; - -use secp::{MaybePoint, MaybeScalar, Point, Scalar, G}; - -use sha2::Digest as _; - -/// Partial signatures are just scalars in the range `[0, n)`. -/// -/// See the documentation of [`secp::MaybeScalar`] for the -/// parsing, serializing, and conversion traits available -/// on this type. -pub type PartialSignature = MaybeScalar; - -/// Computes the challenge hash `e` for for a signature. You probably don't need -/// to call this directly. Instead use [`sign_solo`][crate::sign_solo] or -/// [`sign_partial`][crate::sign_partial]. -pub fn compute_challenge_hash_tweak>( - final_nonce_xonly: &[u8; 32], - aggregated_pubkey: &Point, - message: impl AsRef<[u8]>, -) -> S { - let hash: [u8; 32] = tagged_hashes::BIP0340_CHALLENGE_TAG_HASHER - .clone() - .chain_update(final_nonce_xonly) - .chain_update(aggregated_pubkey.serialize_xonly()) - .chain_update(message.as_ref()) - .finalize() - .into(); - - S::from(MaybeScalar::reduce_from(&hash)) -} - -/// Compute a partial signature on a message encrypted under an adaptor point. -/// -/// The partial signature returned from this function is a potentially-zero -/// scalar value which can then be passed to other signers for verification -/// and aggregation. -/// -/// Once aggregated, the signature must be adapted with the discrete log -/// (secret key) of `adaptor_point` for the signature to be considered valid. -/// -/// Returns an error if the given secret key does not belong to this -/// `key_agg_ctx`. As an added safety, we also verify the partial signature -/// before returning it. -pub fn sign_partial_adaptor>( - key_agg_ctx: &KeyAggContext, - seckey: impl Into, - secnonce: SecNonce, - aggregated_nonce: &AggNonce, - adaptor_point: impl Into, - message: impl AsRef<[u8]>, -) -> Result { - let adaptor_point: MaybePoint = adaptor_point.into(); - let seckey: Scalar = seckey.into(); - let pubkey = seckey.base_point_mul(); - - // As a side-effect, looking up the cached key coefficient also confirms - // the individual key is indeed part of the aggregated key. - let key_coeff = key_agg_ctx - .key_coefficient(pubkey) - .ok_or(SigningError::UnknownKey)?; - - let aggregated_pubkey = key_agg_ctx.pubkey; - let pubnonce = secnonce.public_nonce(); - - let b: MaybeScalar = aggregated_nonce.nonce_coefficient(aggregated_pubkey, &message); - let final_nonce: Point = aggregated_nonce.final_nonce(b); - let adapted_nonce = final_nonce + adaptor_point; - - // `d` is negated if only one of the parity accumulator OR the aggregated pubkey - // has odd parity. - let d = seckey.negate_if(aggregated_pubkey.parity() ^ key_agg_ctx.parity_acc); - - let nonce_x_bytes = adapted_nonce.serialize_xonly(); - let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &aggregated_pubkey, &message); - - // if has_even_Y(R): - // k = k1 + b*k2 - // else: - // k = (n-k1) + b(n-k2) - // = n - (k1 + b*k2) - let secnonce_sum = (secnonce.k1 + b * secnonce.k2).negate_if(adapted_nonce.parity()); - - // s = k + e*a*d - let partial_signature = secnonce_sum + (e * key_coeff * d); - - verify_partial_adaptor( - key_agg_ctx, - partial_signature, - aggregated_nonce, - adaptor_point, - pubkey, - &pubnonce, - &message, - )?; - - Ok(T::from(partial_signature)) -} - -/// Compute a partial signature on a message. -/// -/// The partial signature returned from this function is a potentially-zero -/// scalar value which can then be passed to other signers for verification -/// and aggregation. -/// -/// Returns an error if the given secret key does not belong to this -/// `key_agg_ctx`. As an added safety, we also verify the partial signature -/// before returning it. -/// -/// This is equivalent to invoking [`sign_partial_adaptor`], but passing -/// [`MaybePoint::Infinity`] as the adaptor point. -pub fn sign_partial>( - key_agg_ctx: &KeyAggContext, - seckey: impl Into, - secnonce: SecNonce, - aggregated_nonce: &AggNonce, - message: impl AsRef<[u8]>, -) -> Result { - sign_partial_adaptor( - key_agg_ctx, - seckey, - secnonce, - aggregated_nonce, - MaybePoint::Infinity, - message, - ) -} - -/// Verify a partial signature, usually from an untrusted co-signer, -/// which has been encrypted under an adaptor point. -/// -/// If `verify_partial_adaptor` succeeds for every signature in -/// a signing session, the resulting aggregated signature is guaranteed -/// to be valid once it is adapted with the discrete log (secret key) -/// of `adaptor_point`. -/// -/// Returns an error if the given public key doesn't belong to the -/// `key_agg_ctx`, or if the signature is invalid. -pub fn verify_partial_adaptor( - key_agg_ctx: &KeyAggContext, - partial_signature: impl Into, - aggregated_nonce: &AggNonce, - adaptor_point: impl Into, - individual_pubkey: impl Into, - individual_pubnonce: &PubNonce, - message: impl AsRef<[u8]>, -) -> Result<(), VerifyError> { - let partial_signature: MaybeScalar = partial_signature.into(); - - // As a side-effect, looking up the cached effective key also confirms - // the individual key is indeed part of the aggregated key. - let effective_pubkey: MaybePoint = key_agg_ctx - .effective_pubkey(individual_pubkey) - .ok_or(VerifyError::UnknownKey)?; - - let aggregated_pubkey = key_agg_ctx.pubkey; - - let b: MaybeScalar = aggregated_nonce.nonce_coefficient(aggregated_pubkey, &message); - let final_nonce: Point = aggregated_nonce.final_nonce(b); - let adapted_nonce = final_nonce + adaptor_point.into(); - - let mut effective_nonce = individual_pubnonce.R1 + b * individual_pubnonce.R2; - - // Don't need constant time ops here as adapted_nonce is public. - if adapted_nonce.has_odd_y() { - effective_nonce = -effective_nonce; - } - - let nonce_x_bytes = adapted_nonce.serialize_xonly(); - let e: MaybeScalar = compute_challenge_hash_tweak(&nonce_x_bytes, &aggregated_pubkey, &message); - - // s * G == R + (g * gacc * e * a * P) - let challenge_parity = aggregated_pubkey.parity() ^ key_agg_ctx.parity_acc; - let challenge_point = (e * effective_pubkey).negate_if(challenge_parity); - - if partial_signature * G != effective_nonce + challenge_point { - return Err(VerifyError::BadSignature); - } - - Ok(()) -} - -/// Verify a partial signature, usually from an untrusted co-signer. -/// -/// If `verify_partial` succeeds for every signature in -/// a signing session, the resulting aggregated signature is guaranteed -/// to be valid. -/// -/// This function is effectively the same as invoking [`verify_partial_adaptor`] -/// but passing [`MaybePoint::Infinity`] as the adaptor point. -/// -/// Returns an error if the given public key doesn't belong to the -/// `key_agg_ctx`, or if the signature is invalid. -pub fn verify_partial( - key_agg_ctx: &KeyAggContext, - partial_signature: impl Into, - aggregated_nonce: &AggNonce, - individual_pubkey: impl Into, - individual_pubnonce: &PubNonce, - message: impl AsRef<[u8]>, -) -> Result<(), VerifyError> { - verify_partial_adaptor( - key_agg_ctx, - partial_signature, - aggregated_nonce, - MaybePoint::Infinity, - individual_pubkey, - individual_pubnonce, - message, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::errors::DecodeError; - use crate::testhex; - - #[test] - fn test_partial_sign_and_verify() { - const SIGN_VERIFY_VECTORS: &[u8] = include_bytes!("test_vectors/sign_verify_vectors.json"); - - #[derive(serde::Deserialize)] - struct ValidSignVerifyTestCase { - key_indices: Vec, - nonce_indices: Vec, - aggnonce_index: usize, - msg_index: usize, - signer_index: usize, - expected: MaybeScalar, - } - - #[derive(serde::Deserialize, Clone)] - struct SignError { - signer: Option, - } - - #[derive(serde::Deserialize, Clone)] - struct SignErrorTestCase { - key_indices: Vec, - aggnonce_index: usize, - msg_index: usize, - secnonce_index: usize, - error: SignError, - comment: String, - } - - #[derive(serde::Deserialize)] - struct VerifyFailTestCase { - #[serde(rename = "sig", deserialize_with = "testhex::deserialize")] - partial_signature: Vec, - key_indices: Vec, - nonce_indices: Vec, - msg_index: usize, - signer_index: usize, - comment: String, - } - - #[derive(serde::Deserialize)] - struct SignVerifyVectors { - #[serde(rename = "sk")] - seckey: Scalar, - - #[serde(deserialize_with = "testhex::deserialize_vec")] - pubkeys: Vec<[u8; 33]>, - - #[serde(rename = "secnonces", deserialize_with = "testhex::deserialize_vec")] - secret_nonces: Vec<[u8; 97]>, - - #[serde(rename = "pnonces", deserialize_with = "testhex::deserialize_vec")] - public_nonces: Vec<[u8; 66]>, - - #[serde(rename = "aggnonces", deserialize_with = "testhex::deserialize_vec")] - aggregated_nonces: Vec<[u8; 66]>, - - #[serde(rename = "msgs", deserialize_with = "testhex::deserialize_vec")] - messages: Vec>, - - valid_test_cases: Vec, - sign_error_test_cases: Vec, - verify_fail_test_cases: Vec, - } - - let vectors: SignVerifyVectors = serde_json::from_slice(SIGN_VERIFY_VECTORS) - .expect("error parsing test vectors from sign_verify.json"); - - let secnonce = SecNonce::try_from(vectors.secret_nonces[0].as_ref()) - .expect("error parsing secret nonce"); - - for (test_index, test_case) in vectors.valid_test_cases.into_iter().enumerate() { - let pubkeys: Vec = test_case - .key_indices - .into_iter() - .map(|i| { - Point::try_from(&vectors.pubkeys[i]).unwrap_or_else(|_| { - panic!( - "invalid pubkey used in valid test: {}", - base16ct::lower::encode_string(&vectors.pubkeys[i]) - ) - }) - }) - .collect(); - - let signer_pubkey = pubkeys[test_case.signer_index]; - assert_eq!(signer_pubkey, vectors.seckey.base_point_mul()); - - let aggnonce_bytes = &vectors.aggregated_nonces[test_case.aggnonce_index]; - let aggregated_nonce = AggNonce::from_bytes(aggnonce_bytes).unwrap_or_else(|_| { - panic!( - "invalid aggregated nonce used in valid test case: {}", - base16ct::lower::encode_string(aggnonce_bytes) - ) - }); - - let key_agg_ctx = - KeyAggContext::new(pubkeys).expect("error constructing key aggregation context"); - - let message = &vectors.messages[test_case.msg_index]; - - let partial_signature: PartialSignature = sign_partial( - &key_agg_ctx, - vectors.seckey, - secnonce.clone(), - &aggregated_nonce, - message, - ) - .expect("error during partial signing"); - - assert_eq!( - partial_signature, test_case.expected, - "partial signature does not match expected for test case {}", - test_index, - ); - - let adaptor_secret = MaybeScalar::Valid(vectors.seckey); - let adaptor_point = adaptor_secret * G; - let partial_adaptor_signature: PartialSignature = sign_partial_adaptor( - &key_agg_ctx, - vectors.seckey, - secnonce.clone(), - &aggregated_nonce, - adaptor_point, - message, - ) - .expect("error during partial adaptor signing"); - - let public_nonces: Vec = test_case - .nonce_indices - .into_iter() - .map(|i| { - PubNonce::from_bytes(&vectors.public_nonces[i]).unwrap_or_else(|_| { - panic!( - "invalid pubnonce in valid test: {}", - base16ct::lower::encode_string(&vectors.public_nonces[i]) - ) - }) - }) - .collect(); - - // Ensure the aggregated nonce in the test vector is correct - assert_eq!(&AggNonce::sum(&public_nonces), &aggregated_nonce); - - verify_partial( - &key_agg_ctx, - partial_signature, - &aggregated_nonce, - signer_pubkey, - &public_nonces[test_case.signer_index], - message, - ) - .expect("failed to verify valid partial signature"); - - verify_partial_adaptor( - &key_agg_ctx, - partial_adaptor_signature, - &aggregated_nonce, - adaptor_point, - signer_pubkey, - &public_nonces[test_case.signer_index], - message, - ) - .expect("failed to verify valid partial signature"); - } - - // invalid input test case 0: signer's pubkey is not in the key_agg_ctx - { - let test_case = vectors.sign_error_test_cases[0].clone(); - - let pubkeys: Vec = test_case - .key_indices - .into_iter() - .map(|i| Point::try_from(&vectors.pubkeys[i]).unwrap()) - .collect(); - - let aggnonce_bytes = &vectors.aggregated_nonces[test_case.aggnonce_index]; - let aggregated_nonce = AggNonce::from_bytes(aggnonce_bytes).unwrap(); - - let key_agg_ctx = - KeyAggContext::new(pubkeys).expect("error constructing key aggregation context"); - - let message = &vectors.messages[test_case.msg_index]; - - assert_eq!( - sign_partial::( - &key_agg_ctx, - vectors.seckey, - secnonce.clone(), - &aggregated_nonce, - message, - ), - Err(SigningError::UnknownKey), - "partial signing should fail for pubkey not in key_agg_ctx", - ); - } - - // invalid input test case 1: invalid pubkey - { - let test_case = &vectors.sign_error_test_cases[1]; - for (signer_index, &key_index) in test_case.key_indices.iter().enumerate() { - let result = Point::try_from(&vectors.pubkeys[key_index]); - if signer_index == test_case.error.signer.unwrap() { - assert_eq!( - result, - Err(secp::errors::InvalidPointBytes), - "expected invalid signer pubkey" - ); - } else { - result.expect("expected valid signer pubkey"); - } - } - } - - // invalid input test case 2, 3, and 4: invalid aggnonce - { - for test_case in vectors.sign_error_test_cases[2..5].iter() { - let result = - AggNonce::from_bytes(&vectors.aggregated_nonces[test_case.aggnonce_index]); - - assert_eq!( - result, - Err(DecodeError::from(secp::errors::InvalidPointBytes)), - "{} - invalid AggNonce should fail to decode", - &test_case.comment - ); - } - } - - // invalid input test case 5: invalid secnonce - { - let test_case = &vectors.sign_error_test_cases[5]; - let result = SecNonce::from_bytes(&vectors.secret_nonces[test_case.secnonce_index]); - assert_eq!( - result, - Err(DecodeError::from(secp::errors::InvalidScalarBytes)), - "invalid SecNonce should fail to decode" - ); - } - - // Verification failure test cases 0 and 1: fake signatures - { - for test_case in vectors.verify_fail_test_cases[..2].iter() { - let pubkeys: Vec = test_case - .key_indices - .iter() - .map(|&i| Point::try_from(&vectors.pubkeys[i]).unwrap()) - .collect(); - - let signer_pubkey = pubkeys[test_case.signer_index]; - - let key_agg_ctx = KeyAggContext::new(pubkeys) - .expect("error constructing key aggregation context"); - - let public_nonces: Vec = test_case - .nonce_indices - .iter() - .map(|&i| PubNonce::from_bytes(&vectors.public_nonces[i]).unwrap()) - .collect(); - - let aggregated_nonce = AggNonce::sum(&public_nonces); - - let message = &vectors.messages[test_case.msg_index]; - - let partial_signature = - MaybeScalar::try_from(test_case.partial_signature.as_slice()) - .expect("unexpected invalid partial signature"); - - assert_eq!( - verify_partial( - &key_agg_ctx, - partial_signature, - &aggregated_nonce, - signer_pubkey, - &public_nonces[test_case.signer_index], - message, - ), - Err(VerifyError::BadSignature), - "{} - unexpected success while verifying invalid partial signature", - test_case.comment, - ); - } - } - - // Verification failure test case 2: invalid signature - { - let test_case = &vectors.verify_fail_test_cases[2]; - let result = PartialSignature::try_from(test_case.partial_signature.as_slice()); - assert_eq!( - result, - Err(secp::errors::InvalidScalarBytes), - "unexpected valid partial signature" - ); - } - } - - #[test] - fn test_sign_with_tweaks() { - const TWEAK_VECTORS: &[u8] = include_bytes!("test_vectors/tweak_vectors.json"); - - #[derive(serde::Deserialize)] - struct ValidTweakTestCase { - key_indices: Vec, - nonce_indices: Vec, - tweak_indices: Vec, - is_xonly: Vec, - signer_index: usize, - #[serde(rename = "expected")] - partial_signature: MaybeScalar, - } - - #[derive(serde::Deserialize)] - struct TweakVectors { - #[serde(rename = "sk")] - seckey: Scalar, - pubkeys: Vec, - - #[serde(rename = "secnonce")] - secret_nonces: SecNonce, - - #[serde(rename = "pnonces")] - public_nonces: Vec, - - #[serde(rename = "aggnonce")] - aggregated_nonce: AggNonce, - - #[serde(deserialize_with = "testhex::deserialize_vec")] - tweaks: Vec>, - - #[serde(rename = "msg", deserialize_with = "testhex::deserialize")] - message: Vec, - - valid_test_cases: Vec, - } - - let vectors: TweakVectors = - serde_json::from_slice(TWEAK_VECTORS).expect("failed to parse test_vectors/tweak.json"); - - for test_case in vectors.valid_test_cases { - let pubkeys: Vec = test_case - .key_indices - .into_iter() - .map(|i| vectors.pubkeys[i]) - .collect(); - - let signer_pubkey = pubkeys[test_case.signer_index]; - - let mut key_agg_ctx = - KeyAggContext::new(pubkeys).expect("error creating key aggregation context"); - - key_agg_ctx = test_case - .tweak_indices - .into_iter() - .map(|i| { - Scalar::try_from(vectors.tweaks[i].as_slice()) - .expect("failed to parse valid tweak value") - }) - .zip(test_case.is_xonly) - .fold(key_agg_ctx, |ctx, (tweak, is_xonly)| { - ctx.with_tweak(tweak, is_xonly).unwrap_or_else(|_| { - panic!("failed to tweak key agg context with {:x}", tweak) - }) - }); - - let partial_signature: PartialSignature = sign_partial( - &key_agg_ctx, - vectors.seckey, - vectors.secret_nonces.clone(), - &vectors.aggregated_nonce, - &vectors.message, - ) - .expect("error during partial signing"); - - assert_eq!( - partial_signature, test_case.partial_signature, - "incorrect tweaked partial signature", - ); - - let public_nonces: Vec<&PubNonce> = test_case - .nonce_indices - .into_iter() - .map(|i| &vectors.public_nonces[i]) - .collect(); - - verify_partial( - &key_agg_ctx, - partial_signature, - &vectors.aggregated_nonce, - signer_pubkey, - public_nonces[test_case.signer_index], - &vectors.message, - ) - .expect("failed to verify valid partial signature"); - } - } -} diff --git a/crates/musig2/src/tagged_hashes.rs b/crates/musig2/src/tagged_hashes.rs deleted file mode 100644 index 6d30b399..00000000 --- a/crates/musig2/src/tagged_hashes.rs +++ /dev/null @@ -1,208 +0,0 @@ -//! This module holds declarations for computing -//! [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)-style -//! tagged hashes. -//! -//! A tagged hash is a SHA256 hash which has been prefixed with two copies of -//! the SHA256 hash of a given fixed constant byte string. This has the effect -//! of namespacing the hash to reduce the possibility of collisions. -//! -//! You probably won't need to use these hashes yourself, but if you want to -//! produce a tagged hash, simply clone one of the lazily allocated hash engines -//! declared as statics in this module. This will give you an instance of -//! `sha2::Sha256`. -//! -//! ``` -//! use musig2::tagged_hashes; -//! use sha2::Sha256; -//! use sha2::Digest as _; // Brings trait methods into scope -//! -//! let hash = tagged_hashes::KEYAGG_LIST_TAG_HASHER -//! .clone() -//! .chain_update(b"SomeData") -//! .finalize(); -//! -//! let expected = { -//! let tag_digest = Sha256::digest("KeyAgg list"); -//! Sha256::new() -//! .chain_update(&tag_digest) -//! .chain_update(&tag_digest) -//! .chain_update(b"SomeData") -//! .finalize() -//! }; -//! -//! assert_eq!(hash, expected); -//! ``` - -use once_cell::sync::Lazy; -use sha2::Sha256; - -use sha2::Digest as _; - -fn with_tag_hash_prefix(tag_hash: [u8; 32]) -> Sha256 { - Sha256::new().chain_update(tag_hash).chain_update(tag_hash) -} - -/// sha256(b"KeyAgg list") -const KEYAGG_LIST_TAG_DIGEST: [u8; 32] = [ - 0x48, 0x1C, 0x97, 0x1C, 0x3C, 0x0B, 0x46, 0xD7, 0xF0, 0xB2, 0x75, 0xAE, 0x59, 0x8D, 0x4E, 0x2C, - 0x7E, 0xD7, 0x31, 0x9C, 0x59, 0x4A, 0x5C, 0x6E, 0xC7, 0x9E, 0xA0, 0xD4, 0x99, 0x02, 0x94, 0xF0, -]; - -/// sha256(b"KeyAgg coefficient") -const KEYAGG_COEFF_TAG_DIGEST: [u8; 32] = [ - 0xBF, 0xC9, 0x04, 0x03, 0x4D, 0x1C, 0x88, 0xE8, 0xC8, 0x0E, 0x22, 0xE5, 0x3D, 0x24, 0x56, 0x6D, - 0x64, 0x82, 0x4E, 0xD6, 0x42, 0x72, 0x81, 0xC0, 0x91, 0x00, 0xF9, 0x4D, 0xCD, 0x52, 0xC9, 0x81, -]; - -/// sha256(b"MuSig/aux") -const MUSIG_AUX_TAG_DIGEST: [u8; 32] = [ - 0x40, 0x8F, 0x8C, 0x1F, 0x29, 0x24, 0x21, 0xB5, 0x56, 0x9E, 0xBC, 0x6C, 0xB5, 0xF2, 0xE2, 0x0C, - 0xF1, 0xE3, 0x84, 0x1B, 0x47, 0x43, 0x9F, 0xCC, 0x58, 0x7D, 0x20, 0xE3, 0xC1, 0x7F, 0x08, 0x37, -]; - -/// sha256(b"MuSig/nonce") -const MUSIG_NONCE_TAG_DIGEST: [u8; 32] = [ - 0xF8, 0xC1, 0x0C, 0xBC, 0x61, 0x4E, 0xD1, 0xA0, 0x84, 0xB4, 0x37, 0x05, 0x2B, 0x5D, 0x2C, 0x4B, - 0x50, 0x1A, 0x9D, 0xE7, 0xAA, 0xFB, 0xE3, 0x48, 0xAC, 0xE8, 0x02, 0x6C, 0xA7, 0xFC, 0xB1, 0x7B, -]; - -/// sha256(b"MuSig/noncecoef") -const MUSIG_NONCECOEF_TAG_DIGEST: [u8; 32] = [ - 0x5A, 0x6D, 0x45, 0xF6, 0xDA, 0x29, 0xE6, 0x51, 0xCB, 0x1B, 0xA2, 0xB8, 0xAC, 0x2C, 0xDD, 0x4E, - 0xBC, 0x15, 0xC2, 0xFB, 0xB2, 0x89, 0xF0, 0xCC, 0x82, 0x1B, 0xBF, 0x0A, 0x34, 0x09, 0x5F, 0x32, -]; - -/// sha256(b"BIP0340/aux") -const BIP0340_AUX_TAG_DIGEST: [u8; 32] = [ - 0xF1, 0xEF, 0x4E, 0x5E, 0xC0, 0x63, 0xCA, 0xDA, 0x6D, 0x94, 0xCA, 0xFA, 0x9D, 0x98, 0x7E, 0xA0, - 0x69, 0x26, 0x58, 0x39, 0xEC, 0xC1, 0x1F, 0x97, 0x2D, 0x77, 0xA5, 0x2E, 0xD8, 0xC1, 0xCC, 0x90, -]; - -/// sha256(b"BIP0340/nonce") -const BIP0340_NONCE_TAG_DIGEST: [u8; 32] = [ - 0x07, 0x49, 0x77, 0x34, 0xA7, 0x9B, 0xCB, 0x35, 0x5B, 0x9B, 0x8C, 0x7D, 0x03, 0x4F, 0x12, 0x1C, - 0xF4, 0x34, 0xD7, 0x3E, 0xF7, 0x2D, 0xDA, 0x19, 0x87, 0x00, 0x61, 0xFB, 0x52, 0xBF, 0xEB, 0x2F, -]; - -/// sha256(b"BIP0340/challenge") -const BIP0340_CHALLENGE_TAG_DIGEST: [u8; 32] = [ - 0x7B, 0xB5, 0x2D, 0x7A, 0x9F, 0xEF, 0x58, 0x32, 0x3E, 0xB1, 0xBF, 0x7A, 0x40, 0x7D, 0xB3, 0x82, - 0xD2, 0xF3, 0xF2, 0xD8, 0x1B, 0xB1, 0x22, 0x4F, 0x49, 0xFE, 0x51, 0x8F, 0x6D, 0x48, 0xD3, 0x7C, -]; - -/// sha256(b"BIP0340/batch") -const BIP0340_BATCH_TAG_DIGEST: [u8; 32] = [ - 0x77, 0x06, 0x39, 0x59, 0x84, 0x1F, 0xFA, 0x7B, 0x06, 0x15, 0x4E, 0xE0, 0x47, 0x50, 0x19, 0x40, - 0x36, 0x48, 0x7A, 0xB8, 0x91, 0x96, 0xD0, 0x6E, 0xC7, 0x3E, 0x75, 0x82, 0x90, 0x98, 0x41, 0xB5, -]; - -/// sha256(b"TapTweak") -const TAPROOT_TWEAK_TAG_DIGEST: [u8; 32] = [ - 0xe8, 0x0f, 0xe1, 0x63, 0x9c, 0x9c, 0xa0, 0x50, 0xe3, 0xaf, 0x1b, 0x39, 0xc1, 0x43, 0xc6, 0x3e, - 0x42, 0x9c, 0xbc, 0xeb, 0x15, 0xd9, 0x40, 0xfb, 0xb5, 0xc5, 0xa1, 0xf4, 0xaf, 0x57, 0xc5, 0xe9, -]; - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"KeyAgg list") || sha256(b"KeyAgg list") -/// ``` -pub static KEYAGG_LIST_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(KEYAGG_LIST_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"KeyAgg coefficient") || sha256(b"KeyAgg coefficient") -/// ``` -pub static KEYAGG_COEFF_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(KEYAGG_COEFF_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"MuSig/aux") || sha256(b"MuSig/aux") -/// ``` -pub static MUSIG_AUX_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(MUSIG_AUX_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"MuSig/nonce") || sha256(b"MuSig/nonce") -/// ``` -pub static MUSIG_NONCE_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(MUSIG_NONCE_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"MuSig/noncecoef") || sha256(b"MuSig/noncecoef") -/// ``` -pub static MUSIG_NONCECOEF_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(MUSIG_NONCECOEF_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"BIP0340/aux") || sha256(b"BIP0340/aux") -/// ``` -pub static BIP0340_AUX_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(BIP0340_AUX_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"BIP0340/nonce") || sha256(b"BIP0340/nonce") -/// ``` -pub static BIP0340_NONCE_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(BIP0340_NONCE_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"BIP0340/challenge") || sha256(b"BIP0340/challenge") -/// ``` -pub static BIP0340_CHALLENGE_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(BIP0340_CHALLENGE_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"BIP0340/batch") || sha256(b"BIP0340/batch") -/// ``` -pub static BIP0340_BATCH_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(BIP0340_BATCH_TAG_DIGEST)); - -/// A `sha2::Sha256` hash engine with its state initialized to: -/// -/// ```notrust -/// sha256(b"TapTweak") || sha256(b"TapTweak") -/// ``` -pub static TAPROOT_TWEAK_TAG_HASHER: Lazy = - Lazy::new(|| with_tag_hash_prefix(TAPROOT_TWEAK_TAG_DIGEST)); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_tagged_hash() { - let test_cases = [ - ("KeyAgg list", KEYAGG_LIST_TAG_DIGEST), - ("KeyAgg coefficient", KEYAGG_COEFF_TAG_DIGEST), - ("MuSig/aux", MUSIG_AUX_TAG_DIGEST), - ("MuSig/nonce", MUSIG_NONCE_TAG_DIGEST), - ("MuSig/noncecoef", MUSIG_NONCECOEF_TAG_DIGEST), - ("BIP0340/aux", BIP0340_AUX_TAG_DIGEST), - ("BIP0340/nonce", BIP0340_NONCE_TAG_DIGEST), - ("BIP0340/challenge", BIP0340_CHALLENGE_TAG_DIGEST), - ("BIP0340/batch", BIP0340_BATCH_TAG_DIGEST), // custom - ("TapTweak", TAPROOT_TWEAK_TAG_DIGEST), - ]; - for (tag, declared_hash) in test_cases { - let actual_hash = <[u8; 32]>::from(sha2::Sha256::digest(tag)); - assert_eq!(declared_hash, actual_hash); - } - } -} diff --git a/crates/musig2/src/test_vectors/bip340_vectors.csv b/crates/musig2/src/test_vectors/bip340_vectors.csv deleted file mode 100644 index aa317a3b..00000000 --- a/crates/musig2/src/test_vectors/bip340_vectors.csv +++ /dev/null @@ -1,20 +0,0 @@ -index,secret key,public key,aux_rand,message,signature,verification result,comment -0,0000000000000000000000000000000000000000000000000000000000000003,F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9,0000000000000000000000000000000000000000000000000000000000000000,0000000000000000000000000000000000000000000000000000000000000000,E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0,TRUE, -1,B7E151628AED2A6ABF7158809CF4F3C762E7160F38B4DA56A784D9045190CFEF,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,0000000000000000000000000000000000000000000000000000000000000001,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6896BD60EEAE296DB48A229FF71DFE071BDE413E6D43F917DC8DCF8C78DE33418906D11AC976ABCCB20B091292BFF4EA897EFCB639EA871CFA95F6DE339E4B0A,TRUE, -2,C90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B14E5C9,DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8,C87AA53824B4D7AE2EB035A2B5BBBCCC080E76CDC6D1692C4B0B62D798E6D906,7E2D58D8B3BCDF1ABADEC7829054F90DDA9805AAB56C77333024B9D0A508B75C,5831AAEED7B44BB74E5EAB94BA9D4294C49BCF2A60728D8B4C200F50DD313C1BAB745879A5AD954A72C45A91C3A51D3C7ADEA98D82F8481E0E1E03674A6F3FB7,TRUE, -3,0B432B2677937381AEF05BB02A66ECD012773062CF3FA2549E44F58ED2401710,25D1DFF95105F5253C4022F628A996AD3A0D95FBF21D468A1B33F8C160D8F517,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF,7EB0509757E246F19449885651611CB965ECC1A187DD51B64FDA1EDC9637D5EC97582B9CB13DB3933705B32BA982AF5AF25FD78881EBB32771FC5922EFC66EA3,TRUE,test fails if msg is reduced modulo p or n -4,,D69C3509BB99E412E68B0FE8544E72837DFA30746D8BE2AA65975F29D22DC7B9,,4DF3C3F68FCC83B27E9D42C90431A72499F17875C81A599B566C9889B9696703,00000000000000000000003B78CE563F89A0ED9414F5AA28AD0D96D6795F9C6376AFB1548AF603B3EB45C9F8207DEE1060CB71C04E80F593060B07D28308D7F4,TRUE, -5,,EEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key not on the curve -6,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A14602975563CC27944640AC607CD107AE10923D9EF7A73C643E166BE5EBEAFA34B1AC553E2,FALSE,has_even_y(R) is false -7,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,1FA62E331EDBC21C394792D2AB1100A7B432B013DF3F6FF4F99FCB33E0E1515F28890B3EDB6E7189B630448B515CE4F8622A954CFE545735AAEA5134FCCDB2BD,FALSE,negated message -8,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769961764B3AA9B2FFCB6EF947B6887A226E8D7C93E00C5ED0C1834FF0D0C2E6DA6,FALSE,negated s value -9,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,0000000000000000000000000000000000000000000000000000000000000000123DDA8328AF9C23A94C1FEECFD123BA4FB73476F0D594DCB65C6425BD186051,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 0 -10,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,00000000000000000000000000000000000000000000000000000000000000017615FBAF5AE28864013C099742DEADB4DBA87F11AC6754F93780D5A1837CF197,FALSE,sG - eP is infinite. Test fails in single verification if has_even_y(inf) is defined as true and x(inf) as 1 -11,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,4A298DACAE57395A15D0795DDBFD1DCB564DA82B0F269BC70A74F8220429BA1D69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is not an X coordinate on the curve -12,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F69E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,sig[0:32] is equal to field size -13,,DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E177769FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141,FALSE,sig[32:64] is equal to curve order -14,,FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30,,243F6A8885A308D313198A2E03707344A4093822299F31D0082EFA98EC4E6C89,6CFF5C3BA86C69EA4B7376F31A9BCB4F74C1976089B2D9963DA2E5543E17776969E89B4C5564D00349106B8497785DD7D1D713A8AE82B32FA79D5F7FC407D39B,FALSE,public key is not a valid X coordinate because it exceeds the field size -15,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,,71535DB165ECD9FBBC046E5FFAEA61186BB6AD436732FCCC25291A55895464CF6069CE26BF03466228F19A3A62DB8A649F2D560FAC652827D1AF0574E427AB63,TRUE,message of size 0 (added 2022-12) -16,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,11,08A20A0AFEF64124649232E0693C583AB1B9934AE63B4C3511F3AE1134C6A303EA3173BFEA6683BD101FA5AA5DBC1996FE7CACFC5A577D33EC14564CEC2BACBF,TRUE,message of size 1 (added 2022-12) -17,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,0102030405060708090A0B0C0D0E0F1011,5130F39A4059B43BC7CAC09A19ECE52B5D8699D1A71E3C52DA9AFDB6B50AC370C4A482B77BF960F8681540E25B6771ECE1E5A37FD80E5A51897C5566A97EA5A5,TRUE,message of size 17 (added 2022-12) -18,0340034003400340034003400340034003400340034003400340034003400340,778CAA53B4393AC467774D09497A87224BF9FAB6F6E68B23086497324D6FD117,0000000000000000000000000000000000000000000000000000000000000000,99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999,403B12B0D8555A344175EA7EC746566303321E5DBFA8BE6F091635163ECA79A8585ED3E3170807E7C03B720FC54C7B23897FCBA0E9D0B4A06894CFD249F22367,TRUE,message of size 100 (added 2022-12) diff --git a/crates/musig2/src/test_vectors/key_agg_vectors.json b/crates/musig2/src/test_vectors/key_agg_vectors.json deleted file mode 100644 index b2e623de..00000000 --- a/crates/musig2/src/test_vectors/key_agg_vectors.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "pubkeys": [ - "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", - "020000000000000000000000000000000000000000000000000000000000000005", - "02FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30", - "04F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" - ], - "tweaks": [ - "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", - "252E4BD67410A76CDF933D30EAA1608214037F1B105A013ECCD3C5C184A6110B" - ], - "valid_test_cases": [ - { - "key_indices": [0, 1, 2], - "expected": "90539EEDE565F5D054F32CC0C220126889ED1E5D193BAF15AEF344FE59D4610C" - }, - { - "key_indices": [2, 1, 0], - "expected": "6204DE8B083426DC6EAF9502D27024D53FC826BF7D2012148A0575435DF54B2B" - }, - { - "key_indices": [0, 0, 0], - "expected": "B436E3BAD62B8CD409969A224731C193D051162D8C5AE8B109306127DA3AA935" - }, - { - "key_indices": [0, 0, 1, 1], - "expected": "69BC22BFA5D106306E48A20679DE1D7389386124D07571D0D872686028C26A3E" - } - ], - "error_test_cases": [ - { - "key_indices": [0, 3], - "tweak_indices": [], - "is_xonly": [], - "error": { - "type": "invalid_contribution", - "signer": 1, - "contrib": "pubkey" - }, - "comment": "Invalid public key" - }, - { - "key_indices": [0, 4], - "tweak_indices": [], - "is_xonly": [], - "error": { - "type": "invalid_contribution", - "signer": 1, - "contrib": "pubkey" - }, - "comment": "Public key exceeds field size" - }, - { - "key_indices": [5, 0], - "tweak_indices": [], - "is_xonly": [], - "error": { - "type": "invalid_contribution", - "signer": 0, - "contrib": "pubkey" - }, - "comment": "First byte of public key is not 2 or 3" - }, - { - "key_indices": [0, 1], - "tweak_indices": [0], - "is_xonly": [true], - "error": { - "type": "value", - "message": "The tweak must be less than n." - }, - "comment": "Tweak is out of range" - }, - { - "key_indices": [6], - "tweak_indices": [1], - "is_xonly": [false], - "error": { - "type": "value", - "message": "The result of tweaking cannot be infinity." - }, - "comment": "Intermediate tweaking result is point at infinity" - } - ] -} diff --git a/crates/musig2/src/test_vectors/key_sort_vectors.json b/crates/musig2/src/test_vectors/key_sort_vectors.json deleted file mode 100644 index de088a74..00000000 --- a/crates/musig2/src/test_vectors/key_sort_vectors.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "pubkeys": [ - "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", - "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659", - "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", - "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF", - "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8" - ], - "sorted_pubkeys": [ - "023590A94E768F8E1815C2F24B4D80A8E3149316C3518CE7B7AD338368D038CA66", - "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", - "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EB8", - "02DD308AFEC5777E13121FA72B9CC1B7CC0139715309B086C960E18FD969774EFF", - "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - "03DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" - ] -} diff --git a/crates/musig2/src/test_vectors/nonce_agg_vectors.json b/crates/musig2/src/test_vectors/nonce_agg_vectors.json deleted file mode 100644 index 1c04b881..00000000 --- a/crates/musig2/src/test_vectors/nonce_agg_vectors.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "pnonces": [ - "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E66603BA47FBC1834437B3212E89A84D8425E7BF12E0245D98262268EBDCB385D50641", - "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", - "020151C80F435648DF67A22B749CD798CE54E0321D034B92B709B567D60A42E6660279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", - "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60379BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", - "04FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B833", - "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A60248C264CDD57D3C24D79990B0F865674EB62A0F9018277A95011B41BFC193B831", - "03FF406FFD8ADB9CD29877E4985014F66A59F6CD01C0E88CAA8E5F3166B1F676A602FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" - ], - "valid_test_cases": [ - { - "pnonce_indices": [0, 1], - "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B024725377345BDE0E9C33AF3C43C0A29A9249F2F2956FA8CFEB55C8573D0262DC8" - }, - { - "pnonce_indices": [2, 3], - "expected": "035FE1873B4F2967F52FEA4A06AD5A8ECCBE9D0FD73068012C894E2E87CCB5804B000000000000000000000000000000000000000000000000000000000000000000", - "comment": "Sum of second points encoded in the nonces is point at infinity which is serialized as 33 zero bytes" - } - ], - "error_test_cases": [ - { - "pnonce_indices": [0, 4], - "error": { - "type": "invalid_contribution", - "signer": 1, - "contrib": "pubnonce" - }, - "comment": "Public nonce from signer 1 is invalid due wrong tag, 0x04, in the first half" - }, - { - "pnonce_indices": [5, 1], - "error": { - "type": "invalid_contribution", - "signer": 0, - "contrib": "pubnonce" - }, - "comment": "Public nonce from signer 0 is invalid because the second half does not correspond to an X coordinate" - }, - { - "pnonce_indices": [6, 1], - "error": { - "type": "invalid_contribution", - "signer": 0, - "contrib": "pubnonce" - }, - "comment": "Public nonce from signer 0 is invalid because second half exceeds field size" - } - ] -} diff --git a/crates/musig2/src/test_vectors/nonce_gen_vectors.json b/crates/musig2/src/test_vectors/nonce_gen_vectors.json deleted file mode 100644 index 3a409c50..00000000 --- a/crates/musig2/src/test_vectors/nonce_gen_vectors.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "test_cases": [ - { - "rand": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", - "sk": "0202020202020202020202020202020202020202020202020202020202020202", - "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", - "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", - "msg": "0101010101010101010101010101010101010101010101010101010101010101", - "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", - "expected_secnonce": "B114E502BEAA4E301DD08A50264172C84E41650E6CB726B410C0694D59EFFB6495B5CAF28D045B973D63E3C99A44B807BDE375FD6CB39E46DC4A511708D0E9D2024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", - "expected_pubnonce": "02F7BE7089E8376EB355272368766B17E88E7DB72047D05E56AA881EA52B3B35DF02C29C8046FDD0DED4C7E55869137200FBDBFE2EB654267B6D7013602CAED3115A" - }, - { - "rand": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", - "sk": "0202020202020202020202020202020202020202020202020202020202020202", - "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", - "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", - "msg": "", - "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", - "expected_secnonce": "E862B068500320088138468D47E0E6F147E01B6024244AE45EAC40ACE5929B9F0789E051170B9E705D0B9EB49049A323BBBBB206D8E05C19F46C6228742AA7A9024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", - "expected_pubnonce": "023034FA5E2679F01EE66E12225882A7A48CC66719B1B9D3B6C4DBD743EFEDA2C503F3FD6F01EB3A8E9CB315D73F1F3D287CAFBB44AB321153C6287F407600205109" - }, - { - "rand": "0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F", - "sk": "0202020202020202020202020202020202020202020202020202020202020202", - "pk": "024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", - "aggpk": "0707070707070707070707070707070707070707070707070707070707070707", - "msg": "2626262626262626262626262626262626262626262626262626262626262626262626262626", - "extra_in": "0808080808080808080808080808080808080808080808080808080808080808", - "expected_secnonce": "3221975ACBDEA6820EABF02A02B7F27D3A8EF68EE42787B88CBEFD9AA06AF3632EE85B1A61D8EF31126D4663A00DD96E9D1D4959E72D70FE5EBB6E7696EBA66F024D4B6CD1361032CA9BD2AEB9D900AA4D45D9EAD80AC9423374C451A7254D0766", - "expected_pubnonce": "02E5BBC21C69270F59BD634FCBFA281BE9D76601295345112C58954625BF23793A021307511C79F95D38ACACFF1B4DA98228B77E65AA216AD075E9673286EFB4EAF3" - } - ] -} diff --git a/crates/musig2/src/test_vectors/sig_agg_vectors.json b/crates/musig2/src/test_vectors/sig_agg_vectors.json deleted file mode 100644 index 04a7bc6b..00000000 --- a/crates/musig2/src/test_vectors/sig_agg_vectors.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "pubkeys": [ - "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", - "02D2DC6F5DF7C56ACF38C7FA0AE7A759AE30E19B37359DFDE015872324C7EF6E05", - "03C7FB101D97FF930ACD0C6760852EF64E69083DE0B06AC6335724754BB4B0522C", - "02352433B21E7E05D3B452B81CAE566E06D2E003ECE16D1074AABA4289E0E3D581" - ], - "pnonces": [ - "036E5EE6E28824029FEA3E8A9DDD2C8483F5AF98F7177C3AF3CB6F47CAF8D94AE902DBA67E4A1F3680826172DA15AFB1A8CA85C7C5CC88900905C8DC8C328511B53E", - "03E4F798DA48A76EEC1C9CC5AB7A880FFBA201A5F064E627EC9CB0031D1D58FC5103E06180315C5A522B7EC7C08B69DCD721C313C940819296D0A7AB8E8795AC1F00", - "02C0068FD25523A31578B8077F24F78F5BD5F2422AFF47C1FADA0F36B3CEB6C7D202098A55D1736AA5FCC21CF0729CCE852575C06C081125144763C2C4C4A05C09B6", - "031F5C87DCFBFCF330DEE4311D85E8F1DEA01D87A6F1C14CDFC7E4F1D8C441CFA40277BF176E9F747C34F81B0D9F072B1B404A86F402C2D86CF9EA9E9C69876EA3B9", - "023F7042046E0397822C4144A17F8B63D78748696A46C3B9F0A901D296EC3406C302022B0B464292CF9751D699F10980AC764E6F671EFCA15069BBE62B0D1C62522A", - "02D97DDA5988461DF58C5897444F116A7C74E5711BF77A9446E27806563F3B6C47020CBAD9C363A7737F99FA06B6BE093CEAFF5397316C5AC46915C43767AE867C00" - ], - "tweaks": [ - "B511DA492182A91B0FFB9A98020D55F260AE86D7ECBD0399C7383D59A5F2AF7C", - "A815FE049EE3C5AAB66310477FBC8BCCCAC2F3395F59F921C364ACD78A2F48DC", - "75448A87274B056468B977BE06EB1E9F657577B7320B0A3376EA51FD420D18A8" - ], - "psigs": [ - "B15D2CD3C3D22B04DAE438CE653F6B4ECF042F42CFDED7C41B64AAF9B4AF53FB", - "6193D6AC61B354E9105BBDC8937A3454A6D705B6D57322A5A472A02CE99FCB64", - "9A87D3B79EC67228CB97878B76049B15DBD05B8158D17B5B9114D3C226887505", - "66F82EA90923689B855D36C6B7E032FB9970301481B99E01CDB4D6AC7C347A15", - "4F5AEE41510848A6447DCD1BBC78457EF69024944C87F40250D3EF2C25D33EFE", - "DDEF427BBB847CC027BEFF4EDB01038148917832253EBC355FC33F4A8E2FCCE4", - "97B890A26C981DA8102D3BC294159D171D72810FDF7C6A691DEF02F0F7AF3FDC", - "53FA9E08BA5243CBCB0D797C5EE83BC6728E539EB76C2D0BF0F971EE4E909971", - "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" - ], - "msg": "599C67EA410D005B9DA90817CF03ED3B1C868E4DA4EDF00A5880B0082C237869", - "valid_test_cases": [ - { - "aggnonce": "0341432722C5CD0268D829C702CF0D1CBCE57033EED201FD335191385227C3210C03D377F2D258B64AADC0E16F26462323D701D286046A2EA93365656AFD9875982B", - "nonce_indices": [ - 0, - 1 - ], - "key_indices": [ - 0, - 1 - ], - "tweak_indices": [], - "is_xonly": [], - "psig_indices": [ - 0, - 1 - ], - "expected": "041DA22223CE65C92C9A0D6C2CAC828AAF1EEE56304FEC371DDF91EBB2B9EF0912F1038025857FEDEB3FF696F8B99FA4BB2C5812F6095A2E0004EC99CE18DE1E" - }, - { - "aggnonce": "0224AFD36C902084058B51B5D36676BBA4DC97C775873768E58822F87FE437D792028CB15929099EEE2F5DAE404CD39357591BA32E9AF4E162B8D3E7CB5EFE31CB20", - "nonce_indices": [ - 0, - 2 - ], - "key_indices": [ - 0, - 2 - ], - "tweak_indices": [], - "is_xonly": [], - "psig_indices": [ - 2, - 3 - ], - "expected": "1069B67EC3D2F3C7C08291ACCB17A9C9B8F2819A52EB5DF8726E17E7D6B52E9F01800260A7E9DAC450F4BE522DE4CE12BA91AEAF2B4279219EF74BE1D286ADD9" - }, - { - "aggnonce": "0208C5C438C710F4F96A61E9FF3C37758814B8C3AE12BFEA0ED2C87FF6954FF186020B1816EA104B4FCA2D304D733E0E19CEAD51303FF6420BFD222335CAA402916D", - "nonce_indices": [ - 0, - 3 - ], - "key_indices": [ - 0, - 2 - ], - "tweak_indices": [ - 0 - ], - "is_xonly": [ - false - ], - "psig_indices": [ - 4, - 5 - ], - "expected": "5C558E1DCADE86DA0B2F02626A512E30A22CF5255CAEA7EE32C38E9A71A0E9148BA6C0E6EC7683B64220F0298696F1B878CD47B107B81F7188812D593971E0CC" - }, - { - "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD", - "nonce_indices": [ - 0, - 4 - ], - "key_indices": [ - 0, - 3 - ], - "tweak_indices": [ - 0, - 1, - 2 - ], - "is_xonly": [ - true, - false, - true - ], - "psig_indices": [ - 6, - 7 - ], - "expected": "839B08820B681DBA8DAF4CC7B104E8F2638F9388F8D7A555DC17B6E6971D7426CE07BF6AB01F1DB50E4E33719295F4094572B79868E440FB3DEFD3FAC1DB589E" - } - ], - "error_test_cases": [ - { - "aggnonce": "02B5AD07AFCD99B6D92CB433FBD2A28FDEB98EAE2EB09B6014EF0F8197CD58403302E8616910F9293CF692C49F351DB86B25E352901F0E237BAFDA11F1C1CEF29FFD", - "nonce_indices": [ - 0, - 4 - ], - "key_indices": [ - 0, - 3 - ], - "tweak_indices": [ - 0, - 1, - 2 - ], - "is_xonly": [ - true, - false, - true - ], - "psig_indices": [ - 7, - 8 - ], - "error": { - "type": "invalid_contribution", - "signer": 1 - }, - "comment": "Partial signature is invalid because it exceeds group size" - } - ] -} diff --git a/crates/musig2/src/test_vectors/sign_verify_vectors.json b/crates/musig2/src/test_vectors/sign_verify_vectors.json deleted file mode 100644 index b467640c..00000000 --- a/crates/musig2/src/test_vectors/sign_verify_vectors.json +++ /dev/null @@ -1,212 +0,0 @@ -{ - "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", - "pubkeys": [ - "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", - "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA661", - "020000000000000000000000000000000000000000000000000000000000000007" - ], - "secnonces": [ - "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9" - ], - "pnonces": [ - "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", - "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", - "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046", - "0237C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0387BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", - "0200000000000000000000000000000000000000000000000000000000000000090287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480" - ], - "aggnonces": [ - "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", - "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "048465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", - "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61020000000000000000000000000000000000000000000000000000000000000009", - "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD6102FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC30" - ], - "msgs": [ - "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", - "", - "2626262626262626262626262626262626262626262626262626262626262626262626262626" - ], - "valid_test_cases": [ - { - "key_indices": [0, 1, 2], - "nonce_indices": [0, 1, 2], - "aggnonce_index": 0, - "msg_index": 0, - "signer_index": 0, - "expected": "012ABBCB52B3016AC03AD82395A1A415C48B93DEF78718E62A7A90052FE224FB" - }, - { - "key_indices": [1, 0, 2], - "nonce_indices": [1, 0, 2], - "aggnonce_index": 0, - "msg_index": 0, - "signer_index": 1, - "expected": "9FF2F7AAA856150CC8819254218D3ADEEB0535269051897724F9DB3789513A52" - }, - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "aggnonce_index": 0, - "msg_index": 0, - "signer_index": 2, - "expected": "FA23C359F6FAC4E7796BB93BC9F0532A95468C539BA20FF86D7C76ED92227900" - }, - { - "key_indices": [0, 1], - "nonce_indices": [0, 3], - "aggnonce_index": 1, - "msg_index": 0, - "signer_index": 0, - "expected": "AE386064B26105404798F75DE2EB9AF5EDA5387B064B83D049CB7C5E08879531", - "comment": "Both halves of aggregate nonce correspond to point at infinity" - }, - { - "key_indices": [0, 1, 2], - "nonce_indices": [0, 1, 2], - "aggnonce_index": 0, - "msg_index": 1, - "signer_index": 0, - "expected": "D7D63FFD644CCDA4E62BC2BC0B1D02DD32A1DC3030E155195810231D1037D82D", - "comment": "Empty message" - }, - { - "key_indices": [0, 1, 2], - "nonce_indices": [0, 1, 2], - "aggnonce_index": 0, - "msg_index": 2, - "signer_index": 0, - "expected": "E184351828DA5094A97C79CABDAAA0BFB87608C32E8829A4DF5340A6F243B78C", - "comment": "38-byte message" - } - ], - "sign_error_test_cases": [ - { - "key_indices": [1, 2], - "aggnonce_index": 0, - "msg_index": 0, - "secnonce_index": 0, - "error": { - "type": "value", - "message": "The signer's pubkey must be included in the list of pubkeys." - }, - "comment": "The signers pubkey is not in the list of pubkeys. This test case is optional: it can be skipped by implementations that do not check that the signer's pubkey is included in the list of pubkeys." - }, - { - "key_indices": [1, 0, 3], - "aggnonce_index": 0, - "msg_index": 0, - "secnonce_index": 0, - "error": { - "type": "invalid_contribution", - "signer": 2, - "contrib": "pubkey" - }, - "comment": "Signer 2 provided an invalid public key" - }, - { - "key_indices": [1, 2, 0], - "aggnonce_index": 2, - "msg_index": 0, - "secnonce_index": 0, - "error": { - "type": "invalid_contribution", - "signer": null, - "contrib": "aggnonce" - }, - "comment": "Aggregate nonce is invalid due wrong tag, 0x04, in the first half" - }, - { - "key_indices": [1, 2, 0], - "aggnonce_index": 3, - "msg_index": 0, - "secnonce_index": 0, - "error": { - "type": "invalid_contribution", - "signer": null, - "contrib": "aggnonce" - }, - "comment": "Aggregate nonce is invalid because the second half does not correspond to an X coordinate" - }, - { - "key_indices": [1, 2, 0], - "aggnonce_index": 4, - "msg_index": 0, - "secnonce_index": 0, - "error": { - "type": "invalid_contribution", - "signer": null, - "contrib": "aggnonce" - }, - "comment": "Aggregate nonce is invalid because second half exceeds field size" - }, - { - "key_indices": [0, 1, 2], - "aggnonce_index": 0, - "msg_index": 0, - "signer_index": 0, - "secnonce_index": 1, - "error": { - "type": "value", - "message": "first secnonce value is out of range." - }, - "comment": "Secnonce is invalid which may indicate nonce reuse" - } - ], - "verify_fail_test_cases": [ - { - "sig": "97AC833ADCB1AFA42EBF9E0725616F3C9A0D5B614F6FE283CEAAA37A8FFAF406", - "key_indices": [0, 1, 2], - "nonce_indices": [0, 1, 2], - "msg_index": 0, - "signer_index": 0, - "comment": "Wrong signature (which is equal to the negation of valid signature)" - }, - { - "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", - "key_indices": [0, 1, 2], - "nonce_indices": [0, 1, 2], - "msg_index": 0, - "signer_index": 1, - "comment": "Wrong signer" - }, - { - "sig": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", - "key_indices": [0, 1, 2], - "nonce_indices": [0, 1, 2], - "msg_index": 0, - "signer_index": 0, - "comment": "Signature exceeds group size" - } - ], - "verify_error_test_cases": [ - { - "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", - "key_indices": [0, 1, 2], - "nonce_indices": [4, 1, 2], - "msg_index": 0, - "signer_index": 0, - "error": { - "type": "invalid_contribution", - "signer": 0, - "contrib": "pubnonce" - }, - "comment": "Invalid pubnonce" - }, - { - "sig": "68537CC5234E505BD14061F8DA9E90C220A181855FD8BDB7F127BB12403B4D3B", - "key_indices": [3, 1, 2], - "nonce_indices": [0, 1, 2], - "msg_index": 0, - "signer_index": 0, - "error": { - "type": "invalid_contribution", - "signer": 0, - "contrib": "pubkey" - }, - "comment": "Invalid pubkey" - } - ] -} diff --git a/crates/musig2/src/test_vectors/tweak_vectors.json b/crates/musig2/src/test_vectors/tweak_vectors.json deleted file mode 100644 index d0a7cfe8..00000000 --- a/crates/musig2/src/test_vectors/tweak_vectors.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "sk": "7FB9E0E687ADA1EEBF7ECFE2F21E73EBDB51A7D450948DFE8D76D7F2D1007671", - "pubkeys": [ - "03935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", - "02F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", - "02DFF1D77F2A671C5F36183726DB2341BE58FEAE1DA2DECED843240F7B502BA659" - ], - "secnonce": "508B81A611F100A6B2B6B29656590898AF488BCF2E1F55CF22E5CFB84421FE61FA27FD49B1D50085B481285E1CA205D55C82CC1B31FF5CD54A489829355901F703935F972DA013F80AE011890FA89B67A27B7BE6CCB24D3274D18B2D4067F261A9", - "pnonces": [ - "0337C87821AFD50A8644D820A8F3E02E499C931865C2360FB43D0A0D20DAFE07EA0287BF891D2A6DEAEBADC909352AA9405D1428C15F4B75F04DAE642A95C2548480", - "0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F817980279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", - "032DE2662628C90B03F5E720284EB52FF7D71F4284F627B68A853D78C78E1FFE9303E4C5524E83FFE1493B9077CF1CA6BEB2090C93D930321071AD40B2F44E599046" - ], - "aggnonce": "028465FCF0BBDBCF443AABCCE533D42B4B5A10966AC09A49655E8C42DAAB8FCD61037496A3CC86926D452CAFCFD55D25972CA1675D549310DE296BFF42F72EEEA8C9", - "tweaks": [ - "E8F791FF9225A2AF0102AFFF4A9A723D9612A682A25EBE79802B263CDFCD83BB", - "AE2EA797CC0FE72AC5B97B97F3C6957D7E4199A167A58EB08BCAFFDA70AC0455", - "F52ECBC565B3D8BEA2DFD5B75A4F457E54369809322E4120831626F290FA87E0", - "1969AD73CC177FA0B4FCED6DF1F7BF9907E665FDE9BA196A74FED0A3CF5AEF9D", - "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" - ], - "msg": "F95466D086770E689964664219266FE5ED215C92AE20BAB5C9D79ADDDDF3C0CF", - "valid_test_cases": [ - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "tweak_indices": [0], - "is_xonly": [true], - "signer_index": 2, - "expected": "E28A5C66E61E178C2BA19DB77B6CF9F7E2F0F56C17918CD13135E60CC848FE91", - "comment": "A single x-only tweak" - }, - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "tweak_indices": [0], - "is_xonly": [false], - "signer_index": 2, - "expected": "38B0767798252F21BF5702C48028B095428320F73A4B14DB1E25DE58543D2D2D", - "comment": "A single plain tweak" - }, - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "tweak_indices": [0, 1], - "is_xonly": [false, true], - "signer_index": 2, - "expected": "408A0A21C4A0F5DACAF9646AD6EB6FECD7F7A11F03ED1F48DFFF2185BC2C2408", - "comment": "A plain tweak followed by an x-only tweak" - }, - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "tweak_indices": [0, 1, 2, 3], - "is_xonly": [false, false, true, true], - "signer_index": 2, - "expected": "45ABD206E61E3DF2EC9E264A6FEC8292141A633C28586388235541F9ADE75435", - "comment": "Four tweaks: plain, plain, x-only, x-only." - }, - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "tweak_indices": [0, 1, 2, 3], - "is_xonly": [true, false, true, false], - "signer_index": 2, - "expected": "B255FDCAC27B40C7CE7848E2D3B7BF5EA0ED756DA81565AC804CCCA3E1D5D239", - "comment": "Four tweaks: x-only, plain, x-only, plain. If an implementation prohibits applying plain tweaks after x-only tweaks, it can skip this test vector or return an error." - } - ], - "error_test_cases": [ - { - "key_indices": [1, 2, 0], - "nonce_indices": [1, 2, 0], - "tweak_indices": [4], - "is_xonly": [false], - "signer_index": 2, - "error": { - "type": "value", - "message": "The tweak must be less than n." - }, - "comment": "Tweak is invalid because it exceeds group size" - } - ] -} diff --git a/crates/musig2/src/testhex.rs b/crates/musig2/src/testhex.rs deleted file mode 100644 index ec315e6d..00000000 --- a/crates/musig2/src/testhex.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! This code is used to assist in deserializing test vectors. - -use serde::Deserialize; - -#[derive(Debug)] -pub struct FromBytesError { - pub unexpected: String, - pub expected: String, -} - -pub trait TryFromBytes: Sized { - fn try_from_bytes(bytes: &[u8]) -> Result; -} - -impl TryFromBytes for Vec { - fn try_from_bytes(bytes: &[u8]) -> Result { - Ok(Vec::from(bytes)) - } -} - -impl TryFromBytes for [u8; SIZE] { - fn try_from_bytes(bytes: &[u8]) -> Result { - if bytes.len() != SIZE { - return Err(FromBytesError { - expected: format!("byte vector of length {}", SIZE), - unexpected: format!("byte vector of length {}", bytes.len()), - }); - } - - let mut array = [0; SIZE]; - array[..].clone_from_slice(bytes); - Ok(array) - } -} - -struct HexVisitor; - -impl<'de> serde::de::Visitor<'de> for HexVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("a hex string") - } - - fn visit_str(self, v: &str) -> Result - where - E: serde::de::Error, - { - base16ct::mixed::decode_vec(v) - .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(v), &"a hex string")) - } -} - -struct HexString(pub T); - -impl<'de, T: TryFromBytes> Deserialize<'de> for HexString { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let vec = deserializer.deserialize_str(HexVisitor)?; - - let parsed = T::try_from_bytes(&vec).map_err(|e| { - ::invalid_value( - serde::de::Unexpected::Other(&e.unexpected), - &e.expected.as_str(), - ) - })?; - - Ok(HexString(parsed)) - } -} - -pub fn deserialize<'de, D, T>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, - T: TryFromBytes, -{ - let HexString(value) = HexString::::deserialize(deserializer)?; - Ok(value) -} - -pub fn deserialize_vec<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, - T: TryFromBytes, -{ - let items = >>::deserialize(deserializer)? - .into_iter() - .map(|HexString(value)| value) - .collect(); - Ok(items) -} diff --git a/crates/musig2/tests/fuzz_against_reference_impl.rs b/crates/musig2/tests/fuzz_against_reference_impl.rs deleted file mode 100644 index 68bf0aa7..00000000 --- a/crates/musig2/tests/fuzz_against_reference_impl.rs +++ /dev/null @@ -1,317 +0,0 @@ -use rand::Rng; -use secp::{Point, Scalar}; - -use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce, SCHNORR_SIGNATURE_SIZE}; - -fn run_reference_code(code: &str) -> Vec { - let script = [ - "import reference", - "from binascii import hexlify, unhexlify", - "from sys import stdout\n\n", - ] - .join("\n") - + code; - - let error_message = format!("failed to run reference code:\n{}", code); - - let output = std::process::Command::new("python3") - .arg("-c") - .arg(script) - .output() - .expect(&error_message); - - let stderr = String::from_utf8(output.stderr).unwrap(); - - assert!( - output.status.success(), - "{}\nstderr: {}", - error_message, - stderr - ); - - output.stdout -} - -#[test] -fn test_python_interop() { - let output = String::from_utf8(run_reference_code("print('hello world')")).unwrap(); - assert_eq!(output, "hello world\n"); -} - -fn random_sample_indexes( - rng: &mut R, - iterations: usize, - max_count: usize, - index_ceil: usize, -) -> Vec> { - (0..iterations) - .map(|_| { - let count = rng.gen_range(1..max_count); - (0..count) - .map(|_| rng.gen_range(0..index_ceil)) - .collect::>() - }) - .collect() -} - -/// Runs our key aggregation code against the reference implementation using randomly -/// chosen pubkey inputs. -#[test] -fn test_key_aggregation() { - let mut rng = rand::thread_rng(); - - // Initialize a random array of pubkeys. - let mut all_pubkeys = [Point::generator(); 6]; - for pubkey in &mut all_pubkeys { - *pubkey *= Scalar::random(&mut rng); - } - - const ITERATIONS: usize = 5; - const MAX_PUBKEYS: usize = 8; - - // randomly sample indexes into that array, with at least length 1 - let generated_indexes: Vec> = - random_sample_indexes(&mut rng, ITERATIONS, MAX_PUBKEYS, all_pubkeys.len()); - - let all_pubkeys_json = serde_json::to_string(&all_pubkeys).unwrap(); - let generated_indexes_json = serde_json::to_string(&generated_indexes).unwrap(); - - let reference_code_output = run_reference_code(&format!( - r#" -all_pubkeys = [unhexlify(key) for key in {all_pubkeys_json}] -generated_indexes = {generated_indexes_json} - -for indexes in generated_indexes: - pubkeys = [all_pubkeys[i] for i in indexes] - Q = reference.key_agg(pubkeys)[0] - stdout.buffer.write(reference.cbytes(Q)) -"# - )); - assert_eq!( - reference_code_output.len(), - ITERATIONS * 33, - "expected to receive exactly {} * 33 bytes back from reference impl", - ITERATIONS - ); - - for i in 0..ITERATIONS { - let pubkeys: Vec = generated_indexes[i] - .iter() - .map(|&j| all_pubkeys[j]) - .collect(); - - let expected_pubkey_bytes = &reference_code_output[(i * 33)..(i * 33 + 33)]; - let expected_pubkey = Point::from_slice(expected_pubkey_bytes).unwrap_or_else(|_| { - panic!( - "error decoding aggregated public key from reference implementation: {}", - base16ct::lower::encode_string(expected_pubkey_bytes) - ) - }); - - let pubkeys_json = serde_json::to_string(&pubkeys).unwrap(); - - let our_pubkey: Point = KeyAggContext::new(pubkeys) - .unwrap_or_else(|_| panic!("failed to aggregate pubkeys: {}", pubkeys_json)) - .aggregated_pubkey(); - - assert_eq!( - our_pubkey, expected_pubkey, - "aggregated pubkey does not match reference impl for inputs: {}", - pubkeys_json - ); - } -} - -/// Runs our partial signing and signature aggregation code against -/// the reference implementation. -#[test] -fn test_signing() { - let mut rng = rand::thread_rng(); - - let mut all_seckeys = [Scalar::one(); 4]; - for seckey in &mut all_seckeys { - *seckey = Scalar::random(&mut rng); - } - - let all_pubkeys = all_seckeys - .into_iter() - .map(|sk| sk.base_point_mul()) - .collect::>(); - - let all_nonce_seeds = (0..all_seckeys.len()) - .map(|_| Scalar::random(&mut rng)) - .collect::>(); - - let message = "Welcome to MuSig"; - - const ITERATIONS: usize = 5; - const MAX_SIGNERS: usize = 5; - - let all_key_indexes = - random_sample_indexes(&mut rng, ITERATIONS, MAX_SIGNERS, all_seckeys.len()); - - let all_aggregated_pubkeys = (0..ITERATIONS) - .map(|i| { - let pubkeys = all_key_indexes[i].iter().map(|&j| all_pubkeys[j]); - KeyAggContext::new(pubkeys).unwrap().aggregated_pubkey() - }) - .collect::>(); - - let all_seckeys_json = serde_json::to_string(&all_seckeys).unwrap(); - let all_pubkeys_json = serde_json::to_string(&all_pubkeys).unwrap(); - let all_nonce_seeds_json = serde_json::to_string(&all_nonce_seeds).unwrap(); - let all_key_indexes_json = serde_json::to_string(&all_key_indexes).unwrap(); - let all_aggregated_pubkeys_json = serde_json::to_string(&all_aggregated_pubkeys).unwrap(); - - let reference_code_output = run_reference_code(&format!( - r#" -all_seckeys = [unhexlify(key) for key in {all_seckeys_json}] -all_pubkeys = [unhexlify(key) for key in {all_pubkeys_json}] -all_nonce_seeds = [unhexlify(seed) for seed in {all_nonce_seeds_json}] -all_key_indexes = {all_key_indexes_json} -all_aggregated_pubkeys = [unhexlify(b) for b in {all_aggregated_pubkeys_json}] - -message = b"{message}" - -for i in range({ITERATIONS}): - aggregated_pubkey = all_aggregated_pubkeys[i] - key_indexes = all_key_indexes[i] - seckeys = [all_seckeys[j] for j in key_indexes] - pubkeys = [all_pubkeys[j] for j in key_indexes] - nonce_seeds = [all_nonce_seeds[j] for j in key_indexes] - - nonces = [ - reference.nonce_gen_internal( - nonce_seeds[k], - seckeys[k], - pubkeys[k], - aggregated_pubkey[1:], - message, - k.to_bytes(4, 'big') - ) - for k in range(len(key_indexes)) - ] - - aggnonce = reference.nonce_agg([pubnonce for (_, pubnonce) in nonces]) - session_ctx = (aggnonce, pubkeys, [], [], message) - - partial_signatures = [] - for ((secnonce, _), seckey) in zip(nonces, seckeys): - partial_signature = reference.sign(secnonce, seckey, session_ctx) - stdout.buffer.write(partial_signature) - partial_signatures.append(partial_signature) - - final_signature = reference.partial_sig_agg(partial_signatures, session_ctx) - stdout.buffer.write(final_signature) -"# - )); - - let n_partial_signatures = all_key_indexes - .iter() - .map(|indexes| indexes.len()) - .sum::(); - - assert_eq!( - reference_code_output.len(), - n_partial_signatures * 32 + SCHNORR_SIGNATURE_SIZE * ITERATIONS, - "expected {} partial signatures and {} aggregated signatures from reference \ - implementation, got {} bytes", - n_partial_signatures, - ITERATIONS, - reference_code_output.len() - ); - - let mut cursor = 0usize; - - for i in 0..ITERATIONS { - let key_indexes = all_key_indexes[i].clone(); - let seckeys = key_indexes - .iter() - .map(|&j| all_seckeys[j]) - .collect::>(); - let pubkeys = key_indexes - .iter() - .map(|&j| all_pubkeys[j]) - .collect::>(); - let nonce_seeds = key_indexes - .iter() - .map(|&j| all_nonce_seeds[j]) - .collect::>(); - - let debug_json = serde_json::to_string(&serde_json::json!({ - "seckeys": &seckeys, - "nonce_seeds": &nonce_seeds, - })) - .unwrap(); - - let aggregated_pubkey = all_aggregated_pubkeys[i]; - let key_agg_ctx = KeyAggContext::new(pubkeys).unwrap(); - assert_eq!(key_agg_ctx.aggregated_pubkey::(), aggregated_pubkey); - - let secnonces: Vec = nonce_seeds - .into_iter() - .enumerate() - .map(|(k, seed)| { - SecNonce::generate( - seed.serialize(), - seckeys[k], - aggregated_pubkey, - message, - (k as u32).to_be_bytes(), - ) - }) - .collect(); - - let aggnonce = secnonces - .iter() - .map(|secnonce| secnonce.public_nonce()) - .sum::(); - - let mut partial_signatures = Vec::with_capacity(seckeys.len()); - for k in 0..seckeys.len() { - let our_partial_signature: PartialSignature = musig2::sign_partial( - &key_agg_ctx, - seckeys[k], - secnonces[k].clone(), - &aggnonce, - message, - ) - .unwrap_or_else(|_| { - panic!( - "failed to sign with randomly chosen keys and nonces: {}", - debug_json - ) - }); - - let expected_partial_signature_bytes = &reference_code_output[cursor..cursor + 32]; - cursor += 32; - - assert_eq!( - &our_partial_signature.serialize(), - expected_partial_signature_bytes, - "incorrect partial signature for signer index {} using keys and nonces: {}", - k, - debug_json, - ); - - partial_signatures.push(our_partial_signature); - } - let expected_signature_bytes = - &reference_code_output[cursor..cursor + SCHNORR_SIGNATURE_SIZE]; - cursor += SCHNORR_SIGNATURE_SIZE; - - let our_signature: [u8; SCHNORR_SIGNATURE_SIZE] = musig2::aggregate_partial_signatures( - &key_agg_ctx, - &aggnonce, - partial_signatures, - message, - ) - .expect("error aggregating partial signatures"); - - assert_eq!( - &our_signature, expected_signature_bytes, - "incorrect aggregated signature using keys and nonces: {}", - debug_json - ); - } -} diff --git a/crates/secret-service-client/Cargo.toml b/crates/secret-service-client/Cargo.toml index 87b27a38..fad13496 100644 --- a/crates/secret-service-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] bitcoin.workspace = true kanal.workspace = true -musig2 = { path = "../musig2" } +musig2.workspace = true quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 8df7802c..4b80d381 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -10,7 +10,7 @@ use bitcoin::{hashes::Hash, Txid}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::{schnorr::Signature, Error, PublicKey}, - AggNonce, KeyAggContext, LiftedSignature, PubNonce, + AggNonce, LiftedSignature, PubNonce, }; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, diff --git a/crates/secret-service-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml index a5168bca..5e5504bd 100644 --- a/crates/secret-service-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] bitcoin.workspace = true -musig2 = { path = "../musig2" } +musig2.workspace = true quinn.workspace = true rkyv.workspace = true strata-bridge-primitives.workspace = true diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 9be9655f..315b7efc 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -4,7 +4,7 @@ use bitcoin::Txid; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::{schnorr::Signature, PublicKey}, - AggNonce, KeyAggContext, LiftedSignature, PartialSignature, PubNonce, + AggNonce, LiftedSignature, PartialSignature, PubNonce, }; use quinn::{ConnectionError, ReadExactError, WriteError}; use rkyv::{rancor, Archive, Deserialize, Serialize}; diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 3f09261a..7fb1c510 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -3,10 +3,7 @@ use bitcoin::{ taproot::{ControlBlock, TaprootError}, ScriptBuf, TapNodeHash, }; -use musig2::{ - errors::{RoundContributionError, RoundFinalizeError}, - KeyAggContext, -}; +use musig2::errors::{RoundContributionError, RoundFinalizeError}; use rkyv::{with::Map, Archive, Deserialize, Serialize}; use strata_bridge_primitives::scripts::taproot::TaprootWitness; diff --git a/crates/secret-service-server/Cargo.toml b/crates/secret-service-server/Cargo.toml index 4dde12d0..4a153839 100644 --- a/crates/secret-service-server/Cargo.toml +++ b/crates/secret-service-server/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" bitcoin.workspace = true futures.workspace = true kanal.workspace = true -musig2 = { path = "../musig2" } +musig2.workspace = true parking_lot.workspace = true quinn.workspace = true rkyv.workspace = true diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index cfc2d1db..97f04137 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -2,7 +2,6 @@ pub mod bool_arr; pub mod ms2sm; use std::{ - fmt::Debug, future::Future, io, marker::Sync, @@ -14,7 +13,7 @@ use std::{ use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid}; use ms2sm::Musig2SessionManager; -use musig2::{errors::RoundFinalizeError, KeyAggContext, PartialSignature, PubNonce}; +use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; pub use quinn::rustls; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, @@ -27,8 +26,8 @@ use rkyv::{ use secret_service_proto::{ v1::{ traits::{ - Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, - OperatorSigner, P2PSigner, SecretService, Server, StakeChainPreimages, WotsSigner, + Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, OperatorSigner, + P2PSigner, SecretService, Server, StakeChainPreimages, WotsSigner, }, wire::{ArchivedClientMessage, ServerMessage}, }, @@ -60,16 +59,14 @@ impl Future for ServerHandle { } } -pub fn run_server( +pub fn run_server( c: Config, service: Arc, - round_persister: Arc, ) -> Result> where FirstRound: Musig2SignerFirstRound + 'static, SecondRound: Musig2SignerSecondRound + 'static, Service: SecretService + Sync + 'static, - Persister: RoundPersister + Send + Sync + 'static, { let quic_server_config = ServerConfig::with_crypto(Arc::new( QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, @@ -90,13 +87,7 @@ where incoming.refuse(); } else { tokio::spawn( - conn_handler( - incoming, - service.clone(), - round_persister.clone(), - musig2_sm.clone(), - ) - .instrument(span), + conn_handler(incoming, service.clone(), musig2_sm.clone()).instrument(span), ); } } @@ -105,16 +96,14 @@ where Ok(ServerHandle { main: handle }) } -async fn conn_handler( +async fn conn_handler( incoming: Incoming, service: Arc, - round_persister: Arc, musig2_sm: Arc>>, ) where FirstRound: Musig2SignerFirstRound + 'static, SecondRound: Musig2SignerSecondRound + 'static, Service: SecretService + Sync + 'static, - Persister: RoundPersister + Send + Sync + 'static, { let conn = match incoming.await { Ok(conn) => conn, @@ -143,13 +132,8 @@ async fn conn_handler( request_manager( tx, tokio::spawn( - request_handler( - rx, - service.clone(), - round_persister.clone(), - musig2_sm.clone(), - ) - .instrument(handler_span), + request_handler(rx, service.clone(), musig2_sm.clone()) + .instrument(handler_span), ), ) .instrument(manager_span), @@ -186,17 +170,15 @@ async fn request_manager( } } -async fn request_handler( +async fn request_handler( mut rx: RecvStream, service: Arc, - round_persister: Arc, musig2_sm: Arc>>, ) -> Result where FirstRound: Musig2SignerFirstRound, SecondRound: Musig2SignerSecondRound, Service: SecretService, - Persister: RoundPersister + Send + Sync + 'static, { let len_to_read = { let mut buf = [0; size_of::()]; @@ -267,14 +249,6 @@ where break 'block ServerMessage::OpaqueServerError; }; - if let Err(e) = round_persister - .persist_first_round(write_perm.session_id(), &*write_perm.value().await) - .await - { - error!("failed to persist first round: {e:?}"); - break 'block ServerMessage::OpaqueServerError; - } - ServerMessage::Musig2NewSession(Ok(write_perm.session_id())) } ArchivedClientMessage::Musig2Pubkey => ServerMessage::Musig2Pubkey { @@ -338,18 +312,12 @@ where (Ok(Some(first_round)), Ok(pubkey), Ok(pubnonce)) => { let mut fr = first_round.lock().await; let r = fr.receive_pub_nonce(pubkey, pubnonce).await; - if let Err(e) = round_persister.persist_first_round(session_id, &*fr).await - { - error!("failed to persist first round: {e:?}"); - ServerMessage::OpaqueServerError - } else { - ServerMessage::Musig2FirstRoundReceivePubNonce(r.err()) - } + ServerMessage::Musig2FirstRoundReceivePubNonce(r.err()) } _ => ServerMessage::InvalidClientMessage, } } - ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => 'block: { + ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => { let session_id = session_id.to_native() as usize; let mut sm = musig2_sm.lock().await; let r = sm.transition_first_to_second_round(session_id, *hash).await; @@ -365,19 +333,6 @@ where }, } } else { - let mutex = sm - .second_round(session_id) - .expect("in range") - .expect("transitioned to second round"); - let sr = mutex.lock().await; - if let Err(e) = round_persister.persist_second_round(session_id, &sr).await { - error!("failed to persist second round: {e:?}"); - break 'block ServerMessage::OpaqueServerError; - } - if let Err(e) = round_persister.delete_first_round(session_id).await { - error!("failed to delete first round: {e:?}"); - break 'block ServerMessage::OpaqueServerError; - } ServerMessage::Musig2FirstRoundFinalize(None) } } @@ -454,13 +409,7 @@ where (Ok(Some(sr)), Ok(pubkey), Ok(signature)) => { let mut sr = sr.lock().await; let r = sr.receive_signature(pubkey, signature).await; - if let Err(e) = round_persister.persist_second_round(session_id, &sr).await - { - error!("failed to persist second round: {e:?}"); - ServerMessage::OpaqueServerError - } else { - ServerMessage::Musig2SecondRoundReceiveSignature(r.err()) - } + ServerMessage::Musig2SecondRoundReceiveSignature(r.err()) } _ => ServerMessage::InvalidClientMessage, } @@ -491,41 +440,3 @@ where }, }) } - -pub trait RoundPersister -where - FirstRound: Musig2SignerFirstRound, - SecondRound: Musig2SignerSecondRound, -{ - type Error: Debug; - - fn persist_first_round( - &self, - session_id: Musig2SessionId, - first_round: &FirstRound, - ) -> impl Future> + Send; - - fn persist_second_round( - &self, - session_id: Musig2SessionId, - second_round: &SecondRound, - ) -> impl Future> + Send; - - fn delete_first_round( - &self, - session_id: Musig2SessionId, - ) -> impl Future> + Send; - - fn delete_second_round( - &self, - session_id: Musig2SessionId, - ) -> impl Future> + Send; - - fn load_first_rounds( - &self, - ) -> impl Future, Self::Error>> + Send; - - fn load_second_rounds( - &self, - ) -> impl Future, Self::Error>> + Send; -} diff --git a/crates/secret-service-server/src/ms2sm.rs b/crates/secret-service-server/src/ms2sm.rs index 3a042f8f..ccd12225 100644 --- a/crates/secret-service-server/src/ms2sm.rs +++ b/crates/secret-service-server/src/ms2sm.rs @@ -1,4 +1,4 @@ -use std::{mem::MaybeUninit, ptr, sync::Arc}; +use std::{mem::MaybeUninit, sync::Arc}; use musig2::{errors::RoundFinalizeError, LiftedSignature}; use secret_service_proto::v1::traits::{Musig2SignerFirstRound, Musig2SignerSecondRound, Server}; diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index 3f0a06bd..b889e323 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" bitcoin.workspace = true blake3.workspace = true hkdf = "0.12.4" -musig2 = { path = "../musig2" } +musig2.workspace = true parking_lot.workspace = true rand.workspace = true rcgen = "0.13.2" diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 3e668a89..4bd91393 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -1,15 +1,14 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; use bitcoin::{bip32::Xpriv, Network}; -use musig2::{Ms2Signer, ServerFirstRound, ServerSecondRound, SledRoundPersist}; +use musig2::{Ms2Signer, ServerFirstRound, ServerSecondRound}; use operator::Operator; use p2p::ServerP2PSigner; use rand::Rng; use secret_service_proto::v1::traits::{SecretService, Server}; -use sled::Db; use stakechain::StakeChain; use strata_key_derivation::operator::OperatorKeys; -use tokio::{fs, io, task::spawn_blocking}; +use tokio::{fs, io}; use wots::SeededWotsSigner; pub mod musig2; @@ -20,13 +19,12 @@ pub mod wots; pub struct Service { keys: OperatorKeys, - db: Db, } const NETWORK: Network = Network::Signet; impl Service { - pub async fn load_from_seed_and_db(seed_path: &Path, db_path: PathBuf) -> io::Result { + pub async fn load_from_seed(seed_path: &Path) -> io::Result { let mut seed = [0; 32]; if let Some(parent) = seed_path.parent() { @@ -43,20 +41,9 @@ impl Service { Err(e) => return Err(e), }; - let db = spawn_blocking(move || sled::open(db_path)) - .await - .expect("thread ok")?; - let keys = OperatorKeys::new(&Xpriv::new_master(NETWORK, &seed).expect("valid xpriv")) .expect("valid keychain"); - Ok(Self { keys, db }) - } - - pub fn round_persister(&self) -> io::Result { - Ok(SledRoundPersist::new( - self.db.open_tree(b"musig2_first_rounds")?, - self.db.open_tree(b"musig2_second_rounds")?, - )) + Ok(Self { keys }) } } diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index 6d866acd..53844fb8 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -7,15 +7,11 @@ use musig2::{ FirstRound, KeyAggContext, LiftedSignature, SecNonceSpices, SecondRound, }; use rand::{thread_rng, Rng}; -use rkyv::{rancor, with::Map, Archive, Deserialize, Serialize}; use secret_service_proto::v1::traits::{ - Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, Server, + Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, Server, SignerIdxOutOfBounds, }; -use secret_service_server::RoundPersister; -use sled::Tree; use strata_bridge_primitives::scripts::taproot::TaprootWitness; -use terrors::OneOf; pub struct Ms2Signer { kp: Keypair, @@ -84,134 +80,9 @@ impl Musig2Signer for Ms2Signer { } } -pub struct SledRoundPersist { - first_rounds: Tree, - second_rounds: Tree, -} - -impl SledRoundPersist { - pub fn new(first_rounds: Tree, second_rounds: Tree) -> Self { - Self { - first_rounds, - second_rounds, - } - } -} - -impl RoundPersister for SledRoundPersist { - type Error = OneOf<(rancor::Error, sled::Error)>; - - fn persist_first_round( - &self, - session_id: Musig2SessionId, - first_round: &ServerFirstRound, - ) -> impl Future> + Send { - async move { - let bytes = rkyv::to_bytes::(first_round).map_err(OneOf::new)?; - self.first_rounds - .insert(&session_id.to_be_bytes(), bytes.as_ref()) - .map_err(OneOf::new)?; - self.first_rounds.flush_async().await.map_err(OneOf::new)?; - Ok(()) - } - } - - fn persist_second_round( - &self, - session_id: Musig2SessionId, - second_round: &ServerSecondRound, - ) -> impl Future> + Send { - async move { - let bytes = rkyv::to_bytes::(second_round).map_err(OneOf::new)?; - self.second_rounds - .insert(&session_id.to_be_bytes(), bytes.as_ref()) - .map_err(OneOf::new)?; - self.second_rounds.flush_async().await.map_err(OneOf::new)?; - Ok(()) - } - } - - fn delete_first_round( - &self, - session_id: Musig2SessionId, - ) -> impl Future> + Send { - async move { - self.first_rounds - .remove(&session_id.to_be_bytes()) - .map_err(OneOf::new)?; - self.first_rounds.flush_async().await.map_err(OneOf::new)?; - Ok(()) - } - } - - fn delete_second_round( - &self, - session_id: Musig2SessionId, - ) -> impl Future> + Send { - async move { - self.second_rounds - .remove(&session_id.to_be_bytes()) - .map_err(OneOf::new)?; - self.second_rounds.flush_async().await.map_err(OneOf::new)?; - Ok(()) - } - } - - fn load_first_rounds( - &self, - ) -> impl Future, Self::Error>> + Send - { - async move { - Ok(self - .first_rounds - .iter() - .map(|res| { - let (session_id_bytes, bytes) = res.map_err(OneOf::new)?; - let session_id = Musig2SessionId::from_be_bytes( - session_id_bytes - .as_ref() - .try_into() - .expect("valid session id"), - ); - let first_round = rkyv::from_bytes::(&bytes) - .map_err(OneOf::new)?; - Ok((session_id, first_round)) - }) - .collect::, Self::Error>>()?) - } - } - - fn load_second_rounds( - &self, - ) -> impl Future, Self::Error>> + Send - { - async move { - Ok(self - .second_rounds - .iter() - .map(|res| { - let (session_id_bytes, bytes) = res.map_err(OneOf::new)?; - let session_id = Musig2SessionId::from_be_bytes( - session_id_bytes - .as_ref() - .try_into() - .expect("valid session id"), - ); - let second_round = rkyv::from_bytes::(&bytes) - .map_err(OneOf::new)?; - Ok((session_id, second_round)) - }) - .collect::, Self::Error>>()?) - } - } -} - -#[derive(Archive, Serialize, Deserialize)] pub struct ServerFirstRound { first_round: FirstRound, - #[rkyv(with = Map)] ordered_public_keys: Vec, - #[rkyv(with = musig2::rkyv_wrappers::SecretKey)] seckey: SecretKey, } @@ -271,10 +142,8 @@ impl Musig2SignerFirstRound for ServerFirstRound { } } -#[derive(Archive, Serialize, Deserialize)] pub struct ServerSecondRound { second_round: SecondRound<[u8; 32]>, - #[rkyv(with = Map)] ordered_public_keys: Vec, } diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index edf0abb5..d8b103a5 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -69,19 +69,13 @@ async fn main() { connection_limit: conf.transport.conn_limit, }; - let service = Service::load_from_seed_and_db( + let service = Service::load_from_seed( &conf .seed .unwrap_or(PathBuf::from_str("seed").expect("valid path")), - conf.db - .unwrap_or(PathBuf::from_str("db").expect("valid path")), ) .await .expect("good service"); - let persister = service.round_persister().expect("good persister"); - - run_server(config, service.into(), persister.into()) - .unwrap() - .await; + run_server(config, service.into()).unwrap().await.unwrap(); } From c3485580f542d5f80f818b6fb741208c3fe0bb96 Mon Sep 17 00:00:00 2001 From: Azz Date: Sun, 16 Feb 2025 18:51:17 +0000 Subject: [PATCH 12/30] lock update --- Cargo.lock | 382 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 225 insertions(+), 157 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd53d50d..b8d3e9c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,6 +1260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" dependencies = [ "serde", + "zeroize", ] [[package]] @@ -1273,7 +1274,7 @@ dependencies = [ "bitvm", "blake3", "proptest", - "secp256k1 0.29.1", + "secp256k1", "strata-bridge-primitives", "strata-bridge-test-utils", ] @@ -1507,43 +1508,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" -[[package]] -name = "bdk_chain" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4955734f97b2baed3f36d16ae7c203fdde31ae85391ac44ee3cbcaf0886db5ce" -dependencies = [ - "bdk_core", - "bitcoin", - "miniscript", - "serde", -] - -[[package]] -name = "bdk_core" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b545aea1efc090e4f71f1dd5468090d9f54c3de48002064c04895ef811fbe0b2" -dependencies = [ - "bitcoin", - "hashbrown 0.14.5", - "serde", -] - -[[package]] -name = "bdk_wallet" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a13c947be940d32a91b876fc5223a6d839a40bc219496c5c78af74714b1b3f7" -dependencies = [ - "bdk_chain", - "bitcoin", - "miniscript", - "rand_core", - "serde", - "serde_json", -] - [[package]] name = "bech32" version = "0.11.0" @@ -1615,7 +1579,7 @@ dependencies = [ "bitcoin_hashes", "hex-conservative", "hex_lit", - "secp256k1 0.29.1", + "secp256k1", "serde", ] @@ -1629,7 +1593,7 @@ dependencies = [ "bitcoin", "borsh", "hex-conservative", - "secp256k1 0.29.1", + "secp256k1", "serde", "thiserror 2.0.11", ] @@ -1846,6 +1810,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "constant_time_eq 0.3.1", + "zeroize", ] [[package]] @@ -2515,7 +2480,7 @@ dependencies = [ "crossterm_winapi", "libc", "mio 0.8.11", - "parking_lot", + "parking_lot 0.12.3", "signal-hook", "signal-hook-mio", "winapi 0.3.9", @@ -2614,7 +2579,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.10", ] [[package]] @@ -3256,6 +3221,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -3328,7 +3303,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot", + "parking_lot 0.12.3", ] [[package]] @@ -3394,6 +3369,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "gcd" version = "2.3.0" @@ -4240,7 +4224,7 @@ dependencies = [ "http-body", "http-body-util", "jsonrpsee-types", - "parking_lot", + "parking_lot 0.12.3", "pin-project", "rand", "rustc-hash 2.1.1", @@ -4493,7 +4477,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.8.0", "libc", - "redox_syscall", + "redox_syscall 0.5.8", ] [[package]] @@ -4628,49 +4612,6 @@ dependencies = [ "paste", ] -[[package]] -name = "mi6" -version = "0.1.0" -dependencies = [ - "bdk_wallet", - "mi6-proto", - "musig2 0.2.3", - "tokio", -] - -[[package]] -name = "mi6-client" -version = "0.1.0" -dependencies = [ - "kanal", - "mi6-proto", - "quinn", - "rkyv", - "terrors", - "tokio", - "tracing", -] - -[[package]] -name = "mi6-proto" -version = "0.1.0" -dependencies = [ - "rkyv", -] - -[[package]] -name = "mi6-server" -version = "0.1.0" -dependencies = [ - "kanal", - "mi6-proto", - "quinn", - "rkyv", - "terrors", - "tokio", - "tracing", -] - [[package]] name = "mime" version = "0.3.17" @@ -4691,7 +4632,6 @@ checksum = "5bd3c9608217b0d6fa9c9c8ddd875b85ab72bd4311cfc8db35e1b5a08fc11f4d" dependencies = [ "bech32", "bitcoin", - "serde", ] [[package]] @@ -4802,29 +4742,14 @@ dependencies = [ "hmac", "once_cell", "rand", - "secp 0.3.0", - "secp256k1 0.29.1", + "secp", + "secp256k1", "serde", "serdect", "sha2", "subtle", ] -[[package]] -name = "musig2" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd55e6cfd1ddd92698a3e456432e16310f0810faad83ac732e2466ed0961b46d" -dependencies = [ - "base16ct", - "hmac", - "once_cell", - "secp 0.4.1", - "secp256k1 0.30.0", - "sha2", - "subtle", -] - [[package]] name = "native-tls" version = "0.2.13" @@ -5541,6 +5466,17 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -5548,7 +5484,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi 0.3.9", ] [[package]] @@ -5559,7 +5509,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall", + "redox_syscall 0.5.8", "smallvec", "windows-targets 0.52.6", ] @@ -5629,6 +5579,16 @@ dependencies = [ "sha2", ] +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6085,12 +6045,34 @@ dependencies = [ "rayon", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "recvmsg" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "redox_syscall" version = "0.5.8" @@ -6891,24 +6873,12 @@ dependencies = [ "base16ct", "once_cell", "rand", - "secp256k1 0.29.1", + "secp256k1", "serde", "serdect", "subtle", ] -[[package]] -name = "secp" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ed54b1141d8cec428d8a4abf01282755ba4e4c8a621dd23fa2e0ed761814c2" -dependencies = [ - "base16ct", - "once_cell", - "secp256k1 0.30.0", - "subtle", -] - [[package]] name = "secp256k1" version = "0.29.1" @@ -6922,23 +6892,84 @@ dependencies = [ ] [[package]] -name = "secp256k1" -version = "0.30.0" +name = "secp256k1-sys" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" dependencies = [ - "bitcoin_hashes", + "cc", +] + +[[package]] +name = "secret-service" +version = "0.1.0" +dependencies = [ + "bitcoin", + "blake3", + "hkdf", + "musig2", + "parking_lot 0.12.3", "rand", - "secp256k1-sys", + "rcgen", + "rkyv", + "rustls-pemfile", + "secret-service-proto", + "secret-service-server", + "serde", + "sha2", + "sled", + "strata-bridge-primitives", + "strata-key-derivation", + "terrors", + "tokio", + "toml", + "tracing", + "tracing-subscriber 0.3.19", ] [[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +name = "secret-service-client" +version = "0.1.0" dependencies = [ - "cc", + "bitcoin", + "kanal", + "musig2", + "quinn", + "rkyv", + "secret-service-proto", + "strata-bridge-primitives", + "terrors", + "tokio", + "tracing", +] + +[[package]] +name = "secret-service-proto" +version = "0.1.0" +dependencies = [ + "bitcoin", + "musig2", + "quinn", + "rkyv", + "strata-bridge-primitives", +] + +[[package]] +name = "secret-service-server" +version = "0.1.0" +dependencies = [ + "bitcoin", + "futures", + "kanal", + "musig2", + "parking_lot 0.12.3", + "quinn", + "rkyv", + "secret-service-proto", + "strata-bridge-primitives", + "terrors", + "tokio", + "tracing", ] [[package]] @@ -7140,7 +7171,7 @@ dependencies = [ "futures", "log", "once_cell", - "parking_lot", + "parking_lot 0.12.3", "scc", "serial_test_derive", ] @@ -7274,6 +7305,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + [[package]] name = "smallvec" version = "1.14.0" @@ -8035,7 +8082,7 @@ dependencies = [ "clap", "jsonrpsee", "rand", - "secp256k1 0.29.1", + "secp256k1", "serde_json", "sqlx", "strata-bridge-agent", @@ -8062,10 +8109,10 @@ dependencies = [ "bitvm", "borsh", "jsonrpsee", - "musig2 0.1.0", + "musig2", "rand", "rkyv", - "secp256k1 0.29.1", + "secp256k1", "serde", "serde_json", "sp1-verifier", @@ -8092,9 +8139,9 @@ dependencies = [ "arbitrary", "async-trait", "bitcoin", - "musig2 0.1.0", + "musig2", "rkyv", - "secp256k1 0.29.1", + "secp256k1", "serde_json", "sqlx", "strata-bridge-primitives", @@ -8122,9 +8169,9 @@ dependencies = [ "bitcoin-bosd", "bitcoin-script", "bitvm", - "musig2 0.1.0", + "musig2", "rkyv", - "secp256k1 0.29.1", + "secp256k1", "serde", "sha2", "strata-primitives", @@ -8211,7 +8258,7 @@ version = "0.1.0" source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", - "musig2 0.1.0", + "musig2", "strata-db", "strata-primitives", "strata-storage", @@ -8225,7 +8272,7 @@ version = "0.1.0" dependencies = [ "bitcoin", "corepc-node", - "secp256k1 0.29.1", + "secp256k1", "serde", "strata-bridge-primitives", "strata-bridge-test-utils", @@ -8243,7 +8290,7 @@ dependencies = [ "bitcoin", "bitvm", "corepc-node", - "musig2 0.1.0", + "musig2", "rand_core", "strata-bridge-primitives", "strata-btcio", @@ -8255,7 +8302,7 @@ version = "0.1.0" source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ "bitcoin", - "musig2 0.1.0", + "musig2", "serde", "strata-primitives", "thiserror 2.0.11", @@ -8273,7 +8320,7 @@ dependencies = [ "borsh", "corepc-node", "rkyv", - "secp256k1 0.29.1", + "secp256k1", "serde", "sp1-verifier", "strata-bridge-db", @@ -8300,10 +8347,10 @@ dependencies = [ "bytes", "digest 0.10.7", "hex", - "musig2 0.1.0", + "musig2", "rand", "reqwest", - "secp256k1 0.29.1", + "secp256k1", "serde", "serde_json", "sha2", @@ -8376,7 +8423,7 @@ dependencies = [ "bitcoin", "borsh", "futures", - "secp256k1 0.29.1", + "secp256k1", "strata-btcio", "strata-chaintsn", "strata-common", @@ -8404,7 +8451,7 @@ name = "strata-crypto" version = "0.1.0" source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" dependencies = [ - "secp256k1 0.29.1", + "secp256k1", "sha2", "strata-primitives", ] @@ -8419,8 +8466,8 @@ dependencies = [ "bitcoin", "borsh", "hex", - "musig2 0.1.0", - "parking_lot", + "musig2", + "parking_lot 0.12.3", "serde", "strata-mmr", "strata-primitives", @@ -8440,6 +8487,18 @@ dependencies = [ "thiserror 2.0.11", ] +[[package]] +name = "strata-key-derivation" +version = "0.1.0" +source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +dependencies = [ + "bitcoin", + "secp256k1", + "strata-primitives", + "thiserror 2.0.11", + "zeroize", +] + [[package]] name = "strata-l1tx" version = "0.1.0" @@ -8450,7 +8509,7 @@ dependencies = [ "bitcoin", "borsh", "hex", - "musig2 0.1.0", + "musig2", "strata-bridge-tx-builder", "strata-primitives", "strata-state", @@ -8485,10 +8544,10 @@ dependencies = [ "const-hex", "digest 0.10.7", "hex", - "musig2 0.1.0", + "musig2", "num_enum 0.7.3", "rand", - "secp256k1 0.29.1", + "secp256k1", "serde", "serde_json", "sha2", @@ -8560,7 +8619,7 @@ source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df09 dependencies = [ "anyhow", "borsh", - "parking_lot", + "parking_lot 0.12.3", "serde", "strata-chaintsn", "strata-consensus-logic", @@ -8620,7 +8679,7 @@ dependencies = [ "async-trait", "bitcoin", "lru", - "parking_lot", + "parking_lot 0.12.3", "paste", "strata-db", "strata-primitives", @@ -8975,7 +9034,7 @@ dependencies = [ "bytes", "libc", "mio 1.0.3", - "parking_lot", + "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -9699,7 +9758,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall", + "redox_syscall 0.5.8", "wasite", ] @@ -10037,6 +10096,15 @@ dependencies = [ "rustix", ] +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yoke" version = "0.7.5" From 8104feaf0217c11303665bb46e40a8c24628c012 Mon Sep 17 00:00:00 2001 From: Azz Date: Tue, 18 Feb 2025 12:46:11 +0000 Subject: [PATCH 13/30] wots rework --- Cargo.lock | 64 ++++++++-------- crates/secret-service-client/src/lib.rs | 32 ++++++-- crates/secret-service-proto/src/v1/traits.rs | 14 +++- crates/secret-service-proto/src/v1/wire.rs | 17 +++- crates/secret-service-server/src/lib.rs | 17 +++- crates/secret-service/src/disk/mod.rs | 22 +++++- crates/secret-service/src/disk/wots.rs | 81 +++++++++++++++++--- 7 files changed, 184 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8d3e9c6..a3f214cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2152,9 +2152,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.29" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" +checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" dependencies = [ "clap_builder", "clap_derive", @@ -2162,9 +2162,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.29" +version = "4.5.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" +checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" dependencies = [ "anstream", "anstyle", @@ -8255,7 +8255,7 @@ dependencies = [ [[package]] name = "strata-bridge-sig-manager" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "musig2", @@ -8299,7 +8299,7 @@ dependencies = [ [[package]] name = "strata-bridge-tx-builder" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "musig2", @@ -8338,7 +8338,7 @@ dependencies = [ [[package]] name = "strata-btcio" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "async-trait", @@ -8373,7 +8373,7 @@ dependencies = [ [[package]] name = "strata-chaintsn" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "bitcoin", @@ -8389,7 +8389,7 @@ dependencies = [ [[package]] name = "strata-common" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "deadpool", @@ -8407,7 +8407,7 @@ dependencies = [ [[package]] name = "strata-config" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "serde", @@ -8416,7 +8416,7 @@ dependencies = [ [[package]] name = "strata-consensus-logic" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "async-trait", @@ -8449,7 +8449,7 @@ dependencies = [ [[package]] name = "strata-crypto" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "secp256k1", "sha2", @@ -8459,7 +8459,7 @@ dependencies = [ [[package]] name = "strata-db" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "arbitrary", @@ -8480,7 +8480,7 @@ dependencies = [ [[package]] name = "strata-eectl" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "strata-primitives", "strata-state", @@ -8490,7 +8490,7 @@ dependencies = [ [[package]] name = "strata-key-derivation" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "secp256k1", @@ -8502,7 +8502,7 @@ dependencies = [ [[package]] name = "strata-l1tx" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "arbitrary", @@ -8520,7 +8520,7 @@ dependencies = [ [[package]] name = "strata-mmr" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "arbitrary", "borsh", @@ -8533,7 +8533,7 @@ dependencies = [ [[package]] name = "strata-primitives" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "arbitrary", @@ -8559,7 +8559,7 @@ dependencies = [ [[package]] name = "strata-proofimpl-btc-blockspace" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "borsh", @@ -8576,7 +8576,7 @@ dependencies = [ [[package]] name = "strata-rpc-api" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "hex", @@ -8598,7 +8598,7 @@ dependencies = [ [[package]] name = "strata-rpc-types" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "hex", @@ -8615,7 +8615,7 @@ dependencies = [ [[package]] name = "strata-sequencer" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "borsh", @@ -8638,7 +8638,7 @@ dependencies = [ [[package]] name = "strata-state" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "arbitrary", @@ -8660,7 +8660,7 @@ dependencies = [ [[package]] name = "strata-status" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "strata-primitives", "strata-rpc-types", @@ -8673,7 +8673,7 @@ dependencies = [ [[package]] name = "strata-storage" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "async-trait", @@ -8692,7 +8692,7 @@ dependencies = [ [[package]] name = "strata-tasks" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#42592a9f3ce1ca53c7ed27df090868617493b411" +source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "futures-util", @@ -8882,9 +8882,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.16.0" +version = "3.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" dependencies = [ "cfg-if 1.0.0", "fastrand", @@ -9416,9 +9416,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "ucd-trie" @@ -9526,9 +9526,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" +checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6" [[package]] name = "valuable" diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 4b80d381..c3aac32a 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -473,18 +473,40 @@ struct WotsClient { } impl WotsSigner for WotsClient { - fn get_key( + fn get_160_key( &self, - index: u64, + index: u32, + vout: u32, txid: Txid, - ) -> impl Future::Container<[u8; 64]>> + Send { + ) -> impl Future::Container<[u8; 20 * 160]>> + Send { async move { - let msg = ClientMessage::WotsGetKey { + let msg = ClientMessage::WotsGet160Key { index, + vout, txid: txid.as_raw_hash().to_byte_array(), }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::WotsGetKey { key } = res else { + let ServerMessage::WotsGet160Key { key } = res else { + return Err(ClientError::ProtocolError(res)); + }; + Ok(key) + } + } + + fn get_256_key( + &self, + index: u32, + vout: u32, + txid: Txid, + ) -> impl Future::Container<[u8; 20 * 256]>> + Send { + async move { + let msg = ClientMessage::WotsGet256Key { + index, + vout, + txid: txid.as_raw_hash().to_byte_array(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::WotsGet256Key { key } = res else { return Err(ClientError::ProtocolError(res)); }; Ok(key) diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 315b7efc..7a5a8d42 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -110,11 +110,19 @@ pub trait Musig2SignerSecondRound: Send + Sync { } pub trait WotsSigner: Send { - fn get_key( + fn get_160_key( &self, - index: u64, + index: u32, + vout: u32, txid: Txid, - ) -> impl Future> + Send; + ) -> impl Future> + Send; + + fn get_256_key( + &self, + index: u32, + vout: u32, + txid: Txid, + ) -> impl Future> + Send; } pub trait StakeChainPreimages: Send { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 7fb1c510..cd739162 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -68,8 +68,11 @@ pub enum ServerMessage { ), Musig2SecondRoundFinalize(Musig2SessionResult), - WotsGetKey { - key: [u8; 64], + WotsGet160Key { + key: [u8; 20 * 160], + }, + WotsGet256Key { + key: [u8; 20 * 256], }, StakeChainGetPreimage { @@ -161,8 +164,14 @@ pub enum ClientMessage { session_id: usize, }, - WotsGetKey { - index: u64, + WotsGet160Key { + index: u32, + vout: u32, + txid: [u8; 32], + }, + WotsGet256Key { + index: u32, + vout: u32, txid: [u8; 32], }, diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 97f04137..ef830b88 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -427,10 +427,21 @@ where } } - ArchivedClientMessage::WotsGetKey { index, txid } => { + ArchivedClientMessage::WotsGet160Key { index, vout, txid } => { let txid = Txid::from_slice(txid).expect("correct length"); - let key = service.wots_signer().get_key(index.into(), txid).await; - ServerMessage::WotsGetKey { key } + let key = service + .wots_signer() + .get_160_key(index.into(), vout.into(), txid) + .await; + ServerMessage::WotsGet160Key { key } + } + ArchivedClientMessage::WotsGet256Key { index, vout, txid } => { + let txid = Txid::from_slice(txid).expect("correct length"); + let key = service + .wots_signer() + .get_256_key(index.into(), vout.into(), txid) + .await; + ServerMessage::WotsGet256Key { key } } ArchivedClientMessage::StakeChainGetPreimage { deposit_idx } => { diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 4bd91393..02ce7bb8 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -1,6 +1,10 @@ use std::path::Path; -use bitcoin::{bip32::Xpriv, Network}; +use bitcoin::{ + bip32::{ChildNumber, Xpriv}, + secp256k1::SECP256K1, + Network, +}; use musig2::{Ms2Signer, ServerFirstRound, ServerSecondRound}; use operator::Operator; use p2p::ServerP2PSigner; @@ -67,12 +71,22 @@ impl SecretService for Service { } fn musig2_signer(&self) -> Self::Musig2Signer { - Ms2Signer::new(self.keys.wallet_xpriv().private_key) + let xpriv = self + .keys + .base_xpriv() + .derive_priv( + SECP256K1, + &[ + ChildNumber::from_hardened_idx(20).unwrap(), + ChildNumber::from_hardened_idx(101).unwrap(), + ], + ) + .expect("valid key"); + Ms2Signer::new(xpriv.private_key) } fn wots_signer(&self) -> Self::WotsSigner { - let seed = self.keys.base_xpriv().private_key.secret_bytes(); - SeededWotsSigner::new(seed) + SeededWotsSigner::new(self.keys.base_xpriv()) } fn stake_chain(&self) -> Self::StakeChain { diff --git a/crates/secret-service/src/disk/wots.rs b/crates/secret-service/src/disk/wots.rs index 1ed94632..e2d0e8c2 100644 --- a/crates/secret-service/src/disk/wots.rs +++ b/crates/secret-service/src/disk/wots.rs @@ -1,33 +1,90 @@ use std::future::Future; -use bitcoin::{hashes::Hash, Txid}; +use bitcoin::{ + bip32::{ChildNumber, Xpriv}, + hashes::Hash, + Txid, +}; use hkdf::Hkdf; +use musig2::secp256k1::SECP256K1; use secret_service_proto::v1::traits::{Server, WotsSigner}; use sha2::Sha256; pub struct SeededWotsSigner { - seed: [u8; 32], + ikm_160: [u8; 32], + ikm_256: [u8; 32], } impl SeededWotsSigner { - pub fn new(seed: [u8; 32]) -> Self { - Self { seed } + pub fn new(base: &Xpriv) -> Self { + Self { + ikm_160: base + .derive_priv( + SECP256K1, + &[ + ChildNumber::from_hardened_idx(79).unwrap(), + ChildNumber::from_hardened_idx(160).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ], + ) + .unwrap() + .private_key + .secret_bytes(), + ikm_256: base + .derive_priv( + SECP256K1, + &[ + ChildNumber::from_hardened_idx(79).unwrap(), + ChildNumber::from_hardened_idx(256).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ], + ) + .unwrap() + .private_key + .secret_bytes(), + } } } impl WotsSigner for SeededWotsSigner { - fn get_key(&self, index: u64, txid: Txid) -> impl Future + Send { + fn get_160_key( + &self, + index: u32, + vout: u32, + txid: Txid, + ) -> impl Future + Send { + async move { + let hk = Hkdf::::new(None, &self.ikm_160); + let mut okm = [0u8; 20 * 160]; + let info = { + let mut buf = [0; 40]; + buf[..32].copy_from_slice(txid.as_raw_hash().as_byte_array()); + buf[32..36].copy_from_slice(&vout.to_le_bytes()); + buf[36..].copy_from_slice(&index.to_le_bytes()); + buf + }; + hk.expand(&info, &mut okm).expect("valid output length"); + okm + } + } + + fn get_256_key( + &self, + index: u32, + vout: u32, + txid: Txid, + ) -> impl Future + Send { async move { - let salt = { - let mut buf = [0; 32 + size_of::()]; + let hk = Hkdf::::new(None, &self.ikm_256); + let mut okm = [0u8; 20 * 256]; + let info = { + let mut buf = [0; 40]; buf[..32].copy_from_slice(txid.as_raw_hash().as_byte_array()); - buf[32..].copy_from_slice(&index.to_le_bytes()); + buf[32..36].copy_from_slice(&vout.to_le_bytes()); + buf[36..].copy_from_slice(&index.to_le_bytes()); buf }; - let hk = Hkdf::::new(Some(&salt), &self.seed); - let mut okm = [0u8; 64]; - hk.expand(b"strata-bridge-winternitz", &mut okm) - .expect("64 is a valid length for Sha256 to output"); + hk.expand(&info, &mut okm).expect("valid output length"); okm } } From 7bc616e5185e8d732470c61fe528d168ba0655f2 Mon Sep 17 00:00:00 2001 From: Azz Date: Tue, 18 Feb 2025 13:55:45 +0000 Subject: [PATCH 14/30] stake chain --- crates/secret-service-client/src/lib.rs | 10 ++++- crates/secret-service-proto/src/v1/traits.rs | 8 +++- crates/secret-service-proto/src/v1/wire.rs | 4 +- crates/secret-service-server/src/lib.rs | 15 ++++++- crates/secret-service/src/disk/mod.rs | 3 +- crates/secret-service/src/disk/stakechain.rs | 41 +++++++++++++++++--- 6 files changed, 66 insertions(+), 15 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index c3aac32a..24e08a80 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -522,10 +522,16 @@ struct StakeChainClient { impl StakeChainPreimages for StakeChainClient { fn get_preimg( &self, - deposit_idx: u64, + prestake_txid: Txid, + prestake_vout: u32, + stake_index: u32, ) -> impl Future::Container<[u8; 32]>> + Send { async move { - let msg = ClientMessage::StakeChainGetPreimage { deposit_idx }; + let msg = ClientMessage::StakeChainGetPreimage { + prestake_txid: prestake_txid.to_byte_array(), + prestake_vout, + stake_index, + }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::StakeChainGetPreimage { preimg } = res else { return Err(ClientError::ProtocolError(res)); diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 7a5a8d42..3a78b7da 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -126,8 +126,12 @@ pub trait WotsSigner: Send { } pub trait StakeChainPreimages: Send { - fn get_preimg(&self, deposit_index: u64) - -> impl Future> + Send; + fn get_preimg( + &self, + prestake_txid: Txid, + prestake_vout: u32, + stake_index: u32, + ) -> impl Future> + Send; } pub trait Origin { diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index cd739162..54bd3ead 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -176,7 +176,9 @@ pub enum ClientMessage { }, StakeChainGetPreimage { - deposit_idx: u64, + prestake_txid: [u8; 32], + prestake_vout: u32, + stake_index: u32, }, } diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index ef830b88..4d762b41 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -444,8 +444,19 @@ where ServerMessage::WotsGet256Key { key } } - ArchivedClientMessage::StakeChainGetPreimage { deposit_idx } => { - let preimg = service.stake_chain().get_preimg(deposit_idx.into()).await; + ArchivedClientMessage::StakeChainGetPreimage { + prestake_txid, + prestake_vout, + stake_index, + } => { + let preimg = service + .stake_chain() + .get_preimg( + Txid::from_slice(prestake_txid).expect("correct length"), + prestake_vout.into(), + stake_index.into(), + ) + .await; ServerMessage::StakeChainGetPreimage { preimg } } }, diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 02ce7bb8..0a4cb7ec 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -90,7 +90,6 @@ impl SecretService for Service { } fn stake_chain(&self) -> Self::StakeChain { - let seed = self.keys.base_xpriv().private_key.secret_bytes(); - StakeChain::new(seed) + StakeChain::new(self.keys.base_xpriv()) } } diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs index 74f483b3..3c04f1ad 100644 --- a/crates/secret-service/src/disk/stakechain.rs +++ b/crates/secret-service/src/disk/stakechain.rs @@ -1,25 +1,54 @@ use std::future::Future; +use bitcoin::{ + bip32::{ChildNumber, Xpriv}, + hashes::Hash, + Txid, +}; use hkdf::Hkdf; +use musig2::secp256k1::SECP256K1; use secret_service_proto::v1::traits::{Server, StakeChainPreimages}; use sha2::Sha256; pub struct StakeChain { - seed: [u8; 32], + ikm: [u8; 32], } impl StakeChain { - pub fn new(seed: [u8; 32]) -> Self { - Self { seed } + pub fn new(base: &Xpriv) -> Self { + let xpriv = base + .derive_priv( + SECP256K1, + &[ + ChildNumber::from_hardened_idx(80).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ], + ) + .expect("good child key"); + Self { + ikm: xpriv.private_key.secret_bytes(), + } } } impl StakeChainPreimages for StakeChain { - fn get_preimg(&self, deposit_index: u64) -> impl Future + Send { + fn get_preimg( + &self, + prestake_txid: Txid, + prestake_vout: u32, + stake_index: u32, + ) -> impl Future + Send { async move { - let hk = Hkdf::::new(Some(&deposit_index.to_le_bytes()), &self.seed); + let hk = Hkdf::::new(None, &self.ikm); let mut okm = [0u8; 32]; - hk.expand(b"strata-bridge-stakechain", &mut okm) + let info = { + let mut buf = [0; 40]; + buf[..32].copy_from_slice(&prestake_txid.as_raw_hash().to_byte_array()); + buf[32..36].copy_from_slice(&prestake_vout.to_be_bytes()); + buf[36..].copy_from_slice(&stake_index.to_be_bytes()); + buf + }; + hk.expand(&info, &mut okm) .expect("32 is a valid length for Sha256 to output"); okm } From 9d0b03af13797c044013c5bb11244e56448b2f85 Mon Sep 17 00:00:00 2001 From: Azz Date: Tue, 18 Feb 2025 14:32:43 +0000 Subject: [PATCH 15/30] musig2 --- crates/secret-service-client/src/lib.rs | 4 ++ crates/secret-service-proto/src/v1/traits.rs | 2 + crates/secret-service-proto/src/v1/wire.rs | 2 + crates/secret-service-server/src/lib.rs | 17 ++++++- crates/secret-service/src/disk/mod.rs | 13 +---- crates/secret-service/src/disk/musig2.rs | 51 ++++++++++++++++++-- crates/secret-service/src/disk/stakechain.rs | 4 +- 7 files changed, 74 insertions(+), 19 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 24e08a80..b8580491 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -431,12 +431,16 @@ impl Musig2Signer for Musig2Client { &self, pubkeys: Vec, witness: TaprootWitness, + input_txid: Txid, + input_vout: u32, ) -> impl Future, ClientError>> + Send { async move { let msg = ClientMessage::Musig2NewSession { pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), witness: witness.into(), + input_txid: input_txid.to_byte_array(), + input_vout, }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2NewSession(maybe_session_id) = res else { diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 3a78b7da..232d9e59 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -66,6 +66,8 @@ pub trait Musig2Signer: Send + Sync { &self, pubkeys: Vec, witness: TaprootWitness, + input_txid: Txid, + input_vout: u32, ) -> impl Future>> + Send; fn pubkey(&self) -> impl Future> + Send; } diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 54bd3ead..3d659188 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -121,6 +121,8 @@ pub enum ClientMessage { Musig2NewSession { pubkeys: Vec<[u8; 33]>, witness: SerializableTaprootWitness, + input_txid: [u8; 32], + input_vout: u32, }, Musig2Pubkey, diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 4d762b41..8c4a4bad 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -221,7 +221,12 @@ where } } - ArchivedClientMessage::Musig2NewSession { pubkeys, witness } => 'block: { + ArchivedClientMessage::Musig2NewSession { + pubkeys, + witness, + input_txid, + input_vout, + } => 'block: { let signer = service.musig2_signer(); let Ok(ser_witness) = deserialize::<_, rancor::Error>(witness) else { break 'block ServerMessage::InvalidClientMessage; @@ -239,7 +244,15 @@ where break 'block ServerMessage::InvalidClientMessage; }; - let first_round = match signer.new_session(pubkeys, witness).await { + let first_round = match signer + .new_session( + pubkeys, + witness, + Txid::from_byte_array(*input_txid), + input_vout.into(), + ) + .await + { Ok(fr) => fr, Err(e) => break 'block ServerMessage::Musig2NewSession(Err(e)), }; diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 0a4cb7ec..40385614 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -71,18 +71,7 @@ impl SecretService for Service { } fn musig2_signer(&self) -> Self::Musig2Signer { - let xpriv = self - .keys - .base_xpriv() - .derive_priv( - SECP256K1, - &[ - ChildNumber::from_hardened_idx(20).unwrap(), - ChildNumber::from_hardened_idx(101).unwrap(), - ], - ) - .expect("valid key"); - Ms2Signer::new(xpriv.private_key) + Ms2Signer::new(self.keys.base_xpriv()) } fn wots_signer(&self) -> Self::WotsSigner { diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index 53844fb8..fb14c9e8 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -1,6 +1,12 @@ use std::future::Future; -use bitcoin::key::Keypair; +use bitcoin::{ + bip32::{ChildNumber, Xpriv}, + hashes::Hash, + key::Keypair, + Txid, +}; +use hkdf::Hkdf; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::{PublicKey, SecretKey, SECP256K1}, @@ -11,16 +17,40 @@ use secret_service_proto::v1::traits::{ Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, Server, SignerIdxOutOfBounds, }; +use sha2::Sha256; use strata_bridge_primitives::scripts::taproot::TaprootWitness; pub struct Ms2Signer { kp: Keypair, + ikm: [u8; 32], } impl Ms2Signer { - pub fn new(key: SecretKey) -> Self { + pub fn new(base: &Xpriv) -> Self { + let key = base + .derive_priv( + SECP256K1, + &[ + ChildNumber::from_hardened_idx(20).unwrap(), + ChildNumber::from_hardened_idx(101).unwrap(), + ], + ) + .expect("valid key") + .private_key; + let ikm = base + .derive_priv( + SECP256K1, + &[ + ChildNumber::from_hardened_idx(666).unwrap(), + ChildNumber::from_hardened_idx(0).unwrap(), + ], + ) + .expect("valid child") + .private_key + .secret_bytes(); Self { kp: Keypair::from_secret_key(SECP256K1, &key), + ikm, } } } @@ -30,9 +60,10 @@ impl Musig2Signer for Ms2Signer { &self, mut pubkeys: Vec, witness: TaprootWitness, + input_txid: Txid, + input_vout: u32, ) -> impl Future> + Send { async move { - let nonce_seed = thread_rng().gen::<[u8; 32]>(); if !pubkeys.contains(&self.kp.public_key()) { pubkeys.push(self.kp.public_key()); } @@ -57,6 +88,20 @@ impl Musig2Signer for Ms2Signer { _ => {} } + let nonce_seed = { + let info = { + let mut buf = [0; 36]; + buf[0..32].copy_from_slice(&input_txid.as_raw_hash().to_byte_array()); + buf[32..36].copy_from_slice(&input_vout.to_le_bytes()); + buf + }; + let hk = Hkdf::::new(None, &self.ikm); + let mut okm = [0u8; 32]; + hk.expand(&info, &mut okm) + .expect("32 is a valid length for Sha256 to output"); + okm + }; + let first_round = FirstRound::new( ctx, nonce_seed, diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs index 3c04f1ad..7bb3d0f1 100644 --- a/crates/secret-service/src/disk/stakechain.rs +++ b/crates/secret-service/src/disk/stakechain.rs @@ -44,8 +44,8 @@ impl StakeChainPreimages for StakeChain { let info = { let mut buf = [0; 40]; buf[..32].copy_from_slice(&prestake_txid.as_raw_hash().to_byte_array()); - buf[32..36].copy_from_slice(&prestake_vout.to_be_bytes()); - buf[36..].copy_from_slice(&stake_index.to_be_bytes()); + buf[32..36].copy_from_slice(&prestake_vout.to_le_bytes()); + buf[36..].copy_from_slice(&stake_index.to_le_bytes()); buf }; hk.expand(&info, &mut okm) From 7961121ccb5f74e54abf988d59c5ae79708a9293 Mon Sep 17 00:00:00 2001 From: Azz Date: Wed, 19 Feb 2025 13:09:23 +0000 Subject: [PATCH 16/30] client tls auth --- .gitignore | 3 ++ crates/secret-service-server/src/lib.rs | 68 ++++++++--------------- crates/secret-service/config.toml | 5 ++ crates/secret-service/src/config.rs | 12 ++++- crates/secret-service/src/disk/mod.rs | 15 +++--- crates/secret-service/src/disk/musig2.rs | 1 - crates/secret-service/src/main.rs | 67 +++++++---------------- crates/secret-service/src/tls.rs | 69 ++++++++++++++++++++++++ 8 files changed, 136 insertions(+), 104 deletions(-) create mode 100644 crates/secret-service/config.toml create mode 100644 crates/secret-service/src/tls.rs diff --git a/.gitignore b/.gitignore index a11ad0bd..42576119 100644 --- a/.gitignore +++ b/.gitignore @@ -285,3 +285,6 @@ provers/sp1/proofs/ # test data !test-data/*.bin + +# secret service +seed diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 8c4a4bad..9936b1e3 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -1,15 +1,7 @@ pub mod bool_arr; pub mod ms2sm; -use std::{ - future::Future, - io, - marker::Sync, - net::SocketAddr, - pin::Pin, - sync::Arc, - task::{Context, Poll}, -}; +use std::{io, marker::Sync, net::SocketAddr, sync::Arc}; use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid}; use ms2sm::Musig2SessionManager; @@ -35,10 +27,7 @@ use secret_service_proto::{ }; use strata_bridge_primitives::scripts::taproot::TaprootWitness; use terrors::OneOf; -use tokio::{ - sync::Mutex, - task::{JoinError, JoinHandle}, -}; +use tokio::{sync::Mutex, task::JoinHandle}; use tracing::{error, span, warn, Instrument, Level}; pub struct Config { @@ -47,22 +36,10 @@ pub struct Config { pub tls_config: rustls::ServerConfig, } -pub struct ServerHandle { - main: JoinHandle<()>, -} - -impl Future for ServerHandle { - type Output = Result<(), JoinError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Pin::new(&mut self.get_mut().main).poll(cx) - } -} - -pub fn run_server( +pub async fn run_server( c: Config, service: Arc, -) -> Result> +) -> Result<(), OneOf<(NoInitialCipherSuite, io::Error)>> where FirstRound: Musig2SignerFirstRound + 'static, SecondRound: Musig2SignerSecondRound + 'static, @@ -72,28 +49,25 @@ where QuicServerConfig::try_from(c.tls_config).map_err(OneOf::new)?, )); let endpoint = Endpoint::server(quic_server_config, c.addr).map_err(OneOf::new)?; - let main_task = async move { - let musig2_sm = Arc::new(Mutex::new( - Musig2SessionManager::::default(), - )); - while let Some(incoming) = endpoint.accept().await { - let span = span!(Level::INFO, - "connection", - cid = %incoming.orig_dst_cid(), - remote = %incoming.remote_address(), - remote_validated = %incoming.remote_address_validated() + let musig2_sm = Arc::new(Mutex::new( + Musig2SessionManager::::default(), + )); + while let Some(incoming) = endpoint.accept().await { + let span = span!(Level::INFO, + "connection", + cid = %incoming.orig_dst_cid(), + remote = %incoming.remote_address(), + remote_validated = %incoming.remote_address_validated() + ); + if matches!(c.connection_limit, Some(n) if endpoint.open_connections() >= n) { + incoming.refuse(); + } else { + tokio::spawn( + conn_handler(incoming, service.clone(), musig2_sm.clone()).instrument(span), ); - if matches!(c.connection_limit, Some(n) if endpoint.open_connections() >= n) { - incoming.refuse(); - } else { - tokio::spawn( - conn_handler(incoming, service.clone(), musig2_sm.clone()).instrument(span), - ); - } } - }; - let handle = tokio::spawn(main_task); - Ok(ServerHandle { main: handle }) + } + Ok(()) } async fn conn_handler( diff --git a/crates/secret-service/config.toml b/crates/secret-service/config.toml new file mode 100644 index 00000000..884ad9c3 --- /dev/null +++ b/crates/secret-service/config.toml @@ -0,0 +1,5 @@ +[tls] + +[transport] +addr = "127.0.0.1:8080" +conn_limit = 100 diff --git a/crates/secret-service/src/config.rs b/crates/secret-service/src/config.rs index 00b3373d..8d0c0582 100644 --- a/crates/secret-service/src/config.rs +++ b/crates/secret-service/src/config.rs @@ -2,20 +2,28 @@ use std::{net::SocketAddr, path::PathBuf}; #[derive(serde::Deserialize)] pub struct TomlConfig { - pub tls: Option, + pub tls: TlsConfig, pub transport: TransportConfig, + /// A file path to a 32 byte seed file. pub seed: Option, - pub db: Option, } #[derive(serde::Deserialize)] pub struct TransportConfig { + /// Address to listen on for incoming connections. pub addr: SocketAddr, + /// Maximum number of concurrent connections. pub conn_limit: Option, } +/// Configuration for TLS. #[derive(serde::Deserialize)] pub struct TlsConfig { + /// Path to the certificate file. pub cert: Option, + /// Path to the private key file. pub key: Option, + /// Path to the CA certificate to verify client certificates against. + /// Note that S2 is insecure without client authentication. + pub ca: Option, } diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 40385614..d27883d3 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -1,10 +1,6 @@ use std::path::Path; -use bitcoin::{ - bip32::{ChildNumber, Xpriv}, - secp256k1::SECP256K1, - Network, -}; +use bitcoin::{bip32::Xpriv, Network}; use musig2::{Ms2Signer, ServerFirstRound, ServerSecondRound}; use operator::Operator; use p2p::ServerP2PSigner; @@ -13,6 +9,7 @@ use secret_service_proto::v1::traits::{SecretService, Server}; use stakechain::StakeChain; use strata_key_derivation::operator::OperatorKeys; use tokio::{fs, io}; +use tracing::info; use wots::SeededWotsSigner; pub mod musig2; @@ -36,17 +33,23 @@ impl Service { } match fs::read(seed_path).await { - Ok(vec) => seed.copy_from_slice(&vec), + Ok(vec) => { + seed.copy_from_slice(&vec); + info!("Loaded seed from {}", seed_path.display()); + } Err(e) if e.kind() == io::ErrorKind::NotFound => { let mut rng = rand::thread_rng(); rng.fill(&mut seed); fs::write(seed_path, &seed).await?; + info!("Generated new seed at {}", seed_path.display()); } Err(e) => return Err(e), }; let keys = OperatorKeys::new(&Xpriv::new_master(NETWORK, &seed).expect("valid xpriv")) .expect("valid keychain"); + + info!("Master fingerprint: {}", keys.master_xpub().fingerprint()); Ok(Self { keys }) } } diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index fb14c9e8..f80f434a 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -12,7 +12,6 @@ use musig2::{ secp256k1::{PublicKey, SecretKey, SECP256K1}, FirstRound, KeyAggContext, LiftedSignature, SecNonceSpices, SecondRound, }; -use rand::{thread_rng, Rng}; use secret_service_proto::v1::traits::{ Musig2Signer, Musig2SignerFirstRound, Musig2SignerSecondRound, Origin, Server, SignerIdxOutOfBounds, diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index d8b103a5..bd084d09 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -2,66 +2,32 @@ pub mod config; pub mod disk; +mod tls; -use std::{env::args, path::PathBuf, str::FromStr}; +use std::{env::args, path::PathBuf, str::FromStr, sync::LazyLock}; -use config::{TlsConfig, TomlConfig}; +use config::TomlConfig; use disk::Service; -use secret_service_server::{ - run_server, - rustls::{ - pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, - ServerConfig, - }, - Config, -}; -use tokio::fs; +use secret_service_server::{run_server, Config}; +use tls::load_tls; use tracing::info; +pub static DEV_MODE: LazyLock = + LazyLock::new(|| std::env::var("S2_DEV").is_ok_and(|v| &v == "1")); + #[tokio::main] async fn main() { + tracing_subscriber::fmt::init(); + if *DEV_MODE { + info!("DEV_MODE active"); + } let config_path = PathBuf::from_str(&args().nth(1).unwrap_or_else(|| "config.toml".to_string())) .expect("valid config path"); let text = std::fs::read_to_string(&config_path).expect("read config file"); let conf: TomlConfig = toml::from_str(&text).expect("valid toml"); - - let (certs, key) = if let Some(TlsConfig { - cert: Some(ref crt_path), - key: Some(ref key_path), - }) = conf.tls - { - let key = fs::read(key_path).await.expect("readable key"); - let key = if key_path.extension().is_some_and(|x| x == "der") { - PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) - } else { - rustls_pemfile::private_key(&mut &*key) - .expect("valid PEM-encoded private key") - .expect("non-empty private key") - }; - let cert_chain = fs::read(crt_path).await.expect("readable certificate"); - let cert_chain = if crt_path.extension().is_some_and(|x| x == "der") { - vec![CertificateDer::from(cert_chain)] - } else { - rustls_pemfile::certs(&mut &*cert_chain) - .collect::>() - .expect("valid PEM-encoded certificate") - }; - - (cert_chain, key) - } else { - info!("using self-signed certificate"); - let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); - let key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); - let cert = cert.cert.into(); - (vec![cert], key.into()) - }; - - let tls = ServerConfig::builder() - .with_no_client_auth() - .with_single_cert(certs, key) - .expect("valid rustls config"); + let tls = load_tls(conf.tls).await; let config = Config { addr: conf.transport.addr, @@ -77,5 +43,10 @@ async fn main() { .await .expect("good service"); - run_server(config, service.into()).unwrap().await.unwrap(); + info!("Running on {}", config.addr); + match config.connection_limit { + Some(conn_limit) => info!("Connection limit: {}", conn_limit), + None => info!("No connection limit"), + } + run_server(config, service.into()).await.unwrap(); } diff --git a/crates/secret-service/src/tls.rs b/crates/secret-service/src/tls.rs new file mode 100644 index 00000000..0650b833 --- /dev/null +++ b/crates/secret-service/src/tls.rs @@ -0,0 +1,69 @@ +use std::path::PathBuf; + +use secret_service_server::rustls::{ + pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}, + server::WebPkiClientVerifier, + RootCertStore, ServerConfig, +}; +use tokio::{fs, io}; +use tracing::{error, info, warn}; + +use crate::{config::TlsConfig, DEV_MODE}; + +pub async fn load_tls(conf: TlsConfig) -> ServerConfig { + let (certs, key) = if let (Some(crt_path), Some(key_path)) = (conf.cert, conf.key) { + let key = fs::read(&key_path).await.expect("readable key"); + let key = if key_path.extension().is_some_and(|x| x == "der") { + PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key)) + } else { + rustls_pemfile::private_key(&mut &*key) + .expect("valid PEM-encoded private key") + .expect("non-empty private key") + }; + let cert_chain = read_cert(crt_path).await.expect("valid cert"); + + (cert_chain, key) + } else if *DEV_MODE { + warn!("⚠️ using self-signed certificate"); + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + let cert = cert.cert.into(); + (vec![cert], key.into()) + } else { + error!("TLS configuration is missing certificate and key paths"); + std::process::exit(10); + }; + + let tls_builder = if let Some(ca_path) = conf.ca { + let ca_certs = read_cert(ca_path).await.expect("valid CA cert"); + let mut root_store = RootCertStore::empty(); + let (added, ignored) = root_store.add_parsable_certificates(ca_certs); + info!( + "Added {} certificates to client CA store, ignored {}", + added, ignored + ); + let client_cert_verifier = WebPkiClientVerifier::builder(root_store.into()) + .build() + .expect("valid client verifier"); + ServerConfig::builder().with_client_cert_verifier(client_cert_verifier) + } else if *DEV_MODE { + warn!("⚠️ no CA certificate provided, disabling client authentication"); + ServerConfig::builder().with_no_client_auth() + } else { + error!("TLS configuration is missing CA certificate path"); + std::process::exit(11); + }; + + tls_builder + .with_single_cert(certs, key) + .expect("valid rustls config") +} + +async fn read_cert(path: PathBuf) -> io::Result>> { + let cert_chain = fs::read(&path).await?; + if path.extension().is_some_and(|x| x == "der") { + Ok(vec![CertificateDer::from(cert_chain)]) + } else { + rustls_pemfile::certs(&mut &*cert_chain).collect::>() + } +} From d3da89c02a5f4036119bbc35899a4843a6f2e74b Mon Sep 17 00:00:00 2001 From: Azz Date: Wed, 19 Feb 2025 13:19:40 +0000 Subject: [PATCH 17/30] tracing fixes --- Cargo.lock | 14 ++++++++++++-- crates/secret-service/Cargo.toml | 1 + crates/secret-service/src/disk/mod.rs | 16 +++++++++++++--- crates/secret-service/src/main.rs | 11 ++++++----- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3f214cd..65c37b20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,7 +1241,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ - "colored", + "colored 2.2.0", "num-traits", "rand", "rayon", @@ -1755,7 +1755,7 @@ dependencies = [ "bitcoin-scriptexec", "bitvm-common", "blake3", - "colored", + "colored 2.2.0", "hex", "itertools 0.13.0", "num-bigint 0.4.6", @@ -2206,6 +2206,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -6906,6 +6915,7 @@ version = "0.1.0" dependencies = [ "bitcoin", "blake3", + "colored 3.0.0", "hkdf", "musig2", "parking_lot 0.12.3", diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index b889e323..58ee2ca7 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] bitcoin.workspace = true blake3.workspace = true +colored = "3.0.0" hkdf = "0.12.4" musig2.workspace = true parking_lot.workspace = true diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index d27883d3..070c65f1 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -1,6 +1,7 @@ use std::path::Path; use bitcoin::{bip32::Xpriv, Network}; +use colored::Colorize; use musig2::{Ms2Signer, ServerFirstRound, ServerSecondRound}; use operator::Operator; use p2p::ServerP2PSigner; @@ -35,13 +36,19 @@ impl Service { match fs::read(seed_path).await { Ok(vec) => { seed.copy_from_slice(&vec); - info!("Loaded seed from {}", seed_path.display()); + info!( + "Loaded seed from {}", + seed_path.display().to_string().bold() + ); } Err(e) if e.kind() == io::ErrorKind::NotFound => { let mut rng = rand::thread_rng(); rng.fill(&mut seed); fs::write(seed_path, &seed).await?; - info!("Generated new seed at {}", seed_path.display()); + info!( + "Generated new seed at {}", + seed_path.display().to_string().bold() + ); } Err(e) => return Err(e), }; @@ -49,7 +56,10 @@ impl Service { let keys = OperatorKeys::new(&Xpriv::new_master(NETWORK, &seed).expect("valid xpriv")) .expect("valid keychain"); - info!("Master fingerprint: {}", keys.master_xpub().fingerprint()); + info!( + "Master fingerprint: {}", + keys.master_xpub().fingerprint().to_string().bold() + ); Ok(Self { keys }) } } diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index bd084d09..66887669 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -6,20 +6,21 @@ mod tls; use std::{env::args, path::PathBuf, str::FromStr, sync::LazyLock}; +use colored::Colorize; use config::TomlConfig; use disk::Service; use secret_service_server::{run_server, Config}; use tls::load_tls; -use tracing::info; +use tracing::{info, warn, Level}; pub static DEV_MODE: LazyLock = LazyLock::new(|| std::env::var("S2_DEV").is_ok_and(|v| &v == "1")); #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); + tracing_subscriber::fmt().with_max_level(Level::INFO).init(); if *DEV_MODE { - info!("DEV_MODE active"); + warn!("⚠️ DEV_MODE active"); } let config_path = PathBuf::from_str(&args().nth(1).unwrap_or_else(|| "config.toml".to_string())) @@ -43,9 +44,9 @@ async fn main() { .await .expect("good service"); - info!("Running on {}", config.addr); + info!("Running on {}", config.addr.to_string().bold()); match config.connection_limit { - Some(conn_limit) => info!("Connection limit: {}", conn_limit), + Some(conn_limit) => info!("Connection limit: {}", conn_limit.to_string().bold()), None => info!("No connection limit"), } run_server(config, service.into()).await.unwrap(); From 6f5bb5a93e4182777cb28401e0cfe05ecc617586 Mon Sep 17 00:00:00 2001 From: Azz Date: Thu, 20 Feb 2025 16:23:39 +0000 Subject: [PATCH 18/30] more docs, testing --- Cargo.lock | 123 ++------- crates/secret-service-client/Cargo.toml | 12 +- crates/secret-service-client/src/lib.rs | 4 +- crates/secret-service-proto/Cargo.toml | 10 + crates/secret-service-proto/src/v1/traits.rs | 16 +- crates/secret-service-proto/src/wire.rs | 7 + crates/secret-service-server/Cargo.toml | 13 +- crates/secret-service-server/src/bool_arr.rs | 245 +++++++++++++++++- crates/secret-service-server/src/lib.rs | 16 +- .../src/{ms2sm.rs => musig2_session_mgr.rs} | 41 ++- crates/secret-service/Cargo.toml | 16 +- crates/secret-service/src/disk/mod.rs | 4 +- crates/secret-service/src/disk/musig2.rs | 9 +- crates/secret-service/src/disk/stakechain.rs | 11 +- crates/secret-service/src/disk/wots.rs | 26 +- 15 files changed, 383 insertions(+), 170 deletions(-) rename crates/secret-service-server/src/{ms2sm.rs => musig2_session_mgr.rs} (80%) diff --git a/Cargo.lock b/Cargo.lock index 65c37b20..1dceb948 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,7 +1260,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" dependencies = [ "serde", - "zeroize", ] [[package]] @@ -1810,7 +1809,6 @@ dependencies = [ "cc", "cfg-if 1.0.0", "constant_time_eq 0.3.1", - "zeroize", ] [[package]] @@ -2489,7 +2487,7 @@ dependencies = [ "crossterm_winapi", "libc", "mio 0.8.11", - "parking_lot 0.12.3", + "parking_lot", "signal-hook", "signal-hook-mio", "winapi 0.3.9", @@ -2588,7 +2586,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.10", + "parking_lot_core", ] [[package]] @@ -3230,16 +3228,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "fuchsia-zircon" version = "0.3.3" @@ -3312,7 +3300,7 @@ checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.12.3", + "parking_lot", ] [[package]] @@ -3378,15 +3366,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "gcd" version = "2.3.0" @@ -4233,7 +4212,7 @@ dependencies = [ "http-body", "http-body-util", "jsonrpsee-types", - "parking_lot 0.12.3", + "parking_lot", "pin-project", "rand", "rustc-hash 2.1.1", @@ -4385,16 +4364,6 @@ dependencies = [ "signature", ] -[[package]] -name = "kanal" -version = "0.1.0-pre8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05d55519627edaf7fd0f29981f6dc03fb52df3f5b257130eb8d0bf2801ea1d7" -dependencies = [ - "futures-core", - "lock_api", -] - [[package]] name = "keccak" version = "0.1.5" @@ -4486,7 +4455,7 @@ checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ "bitflags 2.8.0", "libc", - "redox_syscall 0.5.8", + "redox_syscall", ] [[package]] @@ -4537,6 +4506,11 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "make_buf" +version = "1.0.1" +source = "git+https://github.com/alpenlabs/make_buf#bede90c866883153bb5e3705e1d2d1c7602d06b2" + [[package]] name = "malloc_buf" version = "0.0.6" @@ -5475,17 +5449,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -5493,21 +5456,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if 1.0.0", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi 0.3.9", + "parking_lot_core", ] [[package]] @@ -5518,7 +5467,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.5.8", + "redox_syscall", "smallvec", "windows-targets 0.52.6", ] @@ -6073,15 +6022,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.5.8" @@ -6914,23 +6854,19 @@ name = "secret-service" version = "0.1.0" dependencies = [ "bitcoin", - "blake3", "colored 3.0.0", "hkdf", + "make_buf", "musig2", - "parking_lot 0.12.3", "rand", "rcgen", - "rkyv", "rustls-pemfile", "secret-service-proto", "secret-service-server", "serde", "sha2", - "sled", "strata-bridge-primitives", "strata-key-derivation", - "terrors", "tokio", "toml", "tracing", @@ -6942,7 +6878,6 @@ name = "secret-service-client" version = "0.1.0" dependencies = [ "bitcoin", - "kanal", "musig2", "quinn", "rkyv", @@ -6950,7 +6885,6 @@ dependencies = [ "strata-bridge-primitives", "terrors", "tokio", - "tracing", ] [[package]] @@ -6969,10 +6903,7 @@ name = "secret-service-server" version = "0.1.0" dependencies = [ "bitcoin", - "futures", - "kanal", "musig2", - "parking_lot 0.12.3", "quinn", "rkyv", "secret-service-proto", @@ -7181,7 +7112,7 @@ dependencies = [ "futures", "log", "once_cell", - "parking_lot 0.12.3", + "parking_lot", "scc", "serial_test_derive", ] @@ -7315,22 +7246,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "sled" -version = "0.34.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" -dependencies = [ - "crc32fast", - "crossbeam-epoch", - "crossbeam-utils", - "fs2", - "fxhash", - "libc", - "log", - "parking_lot 0.11.2", -] - [[package]] name = "smallvec" version = "1.14.0" @@ -8477,7 +8392,7 @@ dependencies = [ "borsh", "hex", "musig2", - "parking_lot 0.12.3", + "parking_lot", "serde", "strata-mmr", "strata-primitives", @@ -8629,7 +8544,7 @@ source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78d dependencies = [ "anyhow", "borsh", - "parking_lot 0.12.3", + "parking_lot", "serde", "strata-chaintsn", "strata-consensus-logic", @@ -8689,7 +8604,7 @@ dependencies = [ "async-trait", "bitcoin", "lru", - "parking_lot 0.12.3", + "parking_lot", "paste", "strata-db", "strata-primitives", @@ -9044,7 +8959,7 @@ dependencies = [ "bytes", "libc", "mio 1.0.3", - "parking_lot 0.12.3", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -9768,7 +9683,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.5.8", + "redox_syscall", "wasite", ] diff --git a/crates/secret-service-client/Cargo.toml b/crates/secret-service-client/Cargo.toml index fad13496..c52e316e 100644 --- a/crates/secret-service-client/Cargo.toml +++ b/crates/secret-service-client/Cargo.toml @@ -5,7 +5,6 @@ edition = "2021" [dependencies] bitcoin.workspace = true -kanal.workspace = true musig2.workspace = true quinn.workspace = true rkyv.workspace = true @@ -13,4 +12,13 @@ secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } strata-bridge-primitives.workspace = true terrors.workspace = true tokio.workspace = true -tracing.workspace = true + +[lints] +rust.missing_debug_implementations = "warn" +rust.rust_2018_idioms = { level = "deny", priority = -1 } +rust.unreachable_pub = "warn" +rust.unused_crate_dependencies = "deny" +rust.unused_must_use = "deny" +rust.unsafe_op_in_unsafe_fn = "warn" +rust.missing_docs = "warn" +rustdoc.all = "warn" diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index b8580491..26bc3ffb 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -98,7 +98,7 @@ impl SecretService for SecretServic type WotsSigner = WotsClient; - type StakeChain = StakeChainClient; + type StakeChainPreimages = StakeChainClient; fn operator_signer(&self) -> Self::OperatorSigner { OperatorClient { @@ -128,7 +128,7 @@ impl SecretService for SecretServic } } - fn stake_chain(&self) -> Self::StakeChain { + fn stake_chain_preimages(&self) -> Self::StakeChainPreimages { StakeChainClient { conn: self.conn.clone(), config: self.config.clone(), diff --git a/crates/secret-service-proto/Cargo.toml b/crates/secret-service-proto/Cargo.toml index 5e5504bd..fcf70978 100644 --- a/crates/secret-service-proto/Cargo.toml +++ b/crates/secret-service-proto/Cargo.toml @@ -9,3 +9,13 @@ musig2.workspace = true quinn.workspace = true rkyv.workspace = true strata-bridge-primitives.workspace = true + +[lints] +rust.missing_debug_implementations = "warn" +rust.rust_2018_idioms = { level = "deny", priority = -1 } +rust.unreachable_pub = "warn" +rust.unused_crate_dependencies = "deny" +rust.unused_must_use = "deny" +rust.unsafe_op_in_unsafe_fn = "warn" +rust.missing_docs = "warn" +rustdoc.all = "warn" diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 232d9e59..d37a519a 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -12,19 +12,11 @@ use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::wire::ServerMessage; -pub trait SecretServiceFactory: Send + Clone -where - FirstRound: Musig2SignerFirstRound + Clone, - SecondRound: Musig2SignerSecondRound, -{ - type Context: Send + Clone; - type Service: SecretService + Send; - fn produce(ctx: Self::Context) -> Self::Service; -} - // possible when https://github.com/rust-lang/rust/issues/63063 is stabliized // pub type AsyncResult = impl Future>; +/// The SecretService trait is the core interface for the Secret Service, +/// implemented by both the client and the server with different versions. pub trait SecretService: Send where O: Origin, @@ -34,13 +26,13 @@ where type P2PSigner: P2PSigner; type Musig2Signer: Musig2Signer; type WotsSigner: WotsSigner; - type StakeChain: StakeChainPreimages; + type StakeChainPreimages: StakeChainPreimages; fn operator_signer(&self) -> Self::OperatorSigner; fn p2p_signer(&self) -> Self::P2PSigner; fn musig2_signer(&self) -> Self::Musig2Signer; fn wots_signer(&self) -> Self::WotsSigner; - fn stake_chain(&self) -> Self::StakeChain; + fn stake_chain_preimages(&self) -> Self::StakeChainPreimages; } pub trait OperatorSigner: Send { diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs index 3e3d9f5b..b7f445cf 100644 --- a/crates/secret-service-proto/src/wire.rs +++ b/crates/secret-service-proto/src/wire.rs @@ -13,23 +13,30 @@ trait WireMessageMarker: { } +/// A trait for serializing wire messages pub trait WireMessage { + /// Serialize the wire message into an aligned vector using rkyv. fn serialize(&self) -> Result; } +/// The length unit used for wire messages. pub type LengthUint = u16; +/// The global data structure used for wire messages from a client. #[repr(u8)] #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum VersionedClientMessage { + /// Version 1 of the client message. V1(v1::wire::ClientMessage), } impl WireMessageMarker for VersionedClientMessage {} +/// The global data structure used for wire messages from a server. #[repr(u8)] #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum VersionedServerMessage { + /// Version 1 of the server message. V1(v1::wire::ServerMessage), } diff --git a/crates/secret-service-server/Cargo.toml b/crates/secret-service-server/Cargo.toml index 4a153839..f54d54ea 100644 --- a/crates/secret-service-server/Cargo.toml +++ b/crates/secret-service-server/Cargo.toml @@ -5,10 +5,7 @@ edition = "2021" [dependencies] bitcoin.workspace = true -futures.workspace = true -kanal.workspace = true musig2.workspace = true -parking_lot.workspace = true quinn.workspace = true rkyv.workspace = true secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } @@ -16,3 +13,13 @@ strata-bridge-primitives.workspace = true terrors.workspace = true tokio.workspace = true tracing.workspace = true + +[lints] +rust.missing_debug_implementations = "warn" +rust.rust_2018_idioms = { level = "deny", priority = -1 } +rust.unreachable_pub = "warn" +rust.unused_crate_dependencies = "deny" +rust.unused_must_use = "deny" +rust.unsafe_op_in_unsafe_fn = "warn" +rust.missing_docs = "warn" +rustdoc.all = "warn" diff --git a/crates/secret-service-server/src/bool_arr.rs b/crates/secret-service-server/src/bool_arr.rs index d1ee7f78..35644fdc 100644 --- a/crates/secret-service-server/src/bool_arr.rs +++ b/crates/secret-service-server/src/bool_arr.rs @@ -1,17 +1,116 @@ -use std::marker::PhantomData; +//! A space-efficient array implementation for storing types representable as two boolean values. +//! +//! This module provides a [`DoubleBoolArray`] that packs pairs of boolean values into individual +//! bits, allowing efficient storage of enum-like states with four possible variants. Each `u64` +//! chunk stores 32 entries, making it particularly useful for memory-constrained scenarios or +//! when working with large collections of state values. This also allows for efficient iteration +//! over the array for scanning for a slot in a particular state. +//! +//! # Examples +//! ``` +//! use std::convert::Infallible; +//! +//! use secret_service_server::bool_arr::DoubleBoolArray; +//! +//! #[derive(Debug, PartialEq)] +//! enum State { +//! A, +//! B, +//! C, +//! D, +//! } +//! +//! impl From for (bool, bool) { +//! fn from(s: State) -> Self { +//! match s { +//! State::A => (false, false), +//! State::B => (true, false), +//! State::C => (false, true), +//! State::D => (true, true), +//! } +//! } +//! } +//! +//! impl TryFrom<(bool, bool)> for State { +//! type Error = Infallible; +//! fn try_from((b1, b2): (bool, bool)) -> Result { +//! Ok(match (b1, b2) { +//! (false, false) => State::A, +//! (true, false) => State::B, +//! (false, true) => State::C, +//! (true, true) => State::D, +//! }) +//! } +//! } +//! +//! let mut arr = DoubleBoolArray::<2, State>::default(); +//! arr.set(0, State::B); +//! arr.set(31, State::C); +//! assert_eq!(arr.get(0), State::B); +//! ``` -// Kind of like a bitmap for storing bools as bits, but instead we're storing -// (bool, bool) instead of just a bool, allowing us to check up to 4 different -// states. Each 64 bit word can store 32 of these slots. +use std::{ + fmt::{self, Debug}, + marker::PhantomData, +}; + +/// Compact storage for types representable as two boolean values (four possible states). +/// +/// Each entry is stored as two bits, with the following mapping: +/// - Bit 0: First boolean value (LSB) +/// - Bit 1: Second boolean value +/// +/// The generic type `T` must implement bidirectional conversion to/from `(bool, bool)`. +/// IMPORTANT: When T is `(false, false)`, it represents an empty state. +/// +/// # Type Parameters +/// - `N`: Number of `u64` chunks used for storage (capacity = N × 32) +/// - `T`: Stored type that can be converted to/from `(bool, bool)` pairs +/// +/// # Implementation Details +/// - Stores values in N `u64` integers (8N bytes total) +/// - Provides O(1) access time for get/set operations +/// - Implements space-efficient storage with 2 bits per entry pub struct DoubleBoolArray([u64; N], PhantomData) where - T: Into<(bool, bool)> + TryFrom<(bool, bool)>, - >::Error: std::fmt::Debug; + T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, + >::Error: Debug; + +impl fmt::Debug for DoubleBoolArray +where + T: Into<(bool, bool)> + TryFrom<(bool, bool)> + fmt::Debug, + >::Error: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct DebugValues<'a, const N: usize, T>(&'a DoubleBoolArray) + where + T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, + >::Error: Debug; + + impl<'a, const N: usize, T> fmt::Debug for DebugValues<'a, N, T> + where + T: Into<(bool, bool)> + TryFrom<(bool, bool)> + fmt::Debug, + >::Error: fmt::Debug, + { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut list = f.debug_list(); + for i in 0..DoubleBoolArray::::capacity() { + list.entry(&self.0.get(i)); + } + list.finish() + } + } + + f.debug_struct("DoubleBoolArray") + .field("values", &DebugValues(self)) + .finish() + } +} impl Default for DoubleBoolArray where - T: Into<(bool, bool)> + TryFrom<(bool, bool)>, - >::Error: std::fmt::Debug, + T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, + >::Error: Debug, { fn default() -> Self { Self([0; N], PhantomData) @@ -20,18 +119,22 @@ where impl DoubleBoolArray where - T: Into<(bool, bool)> + TryFrom<(bool, bool)>, - >::Error: std::fmt::Debug, + T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, + >::Error: Debug, { + /// Returns the capacity of the array in terms of the number of (bool, bool) slots it can hold. pub const fn capacity() -> usize { N * (std::mem::size_of::() * 8 / 2) } - pub fn find_next_empty_slot(&self) -> Option { + /// Find the index of the first slot with the specified value. + pub fn find_first_slot_with(&self, target: T) -> Option { + let (target_0, target_1) = target.into(); + let target = (target_0 as u64) | ((target_1 as u64) << 1); for (chunk_idx, &chunk) in self.0.iter().enumerate() { for slot in 0..32 { let mask = 0b11 << (slot * 2); - if (chunk & mask) == 0 { + if (chunk & mask) == target { return Some(chunk_idx * 32 + slot); } } @@ -67,3 +170,121 @@ where *chunk = (*chunk & mask) | (new_bits << (slot * 2)); } } + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use super::*; + + #[derive(Debug, PartialEq, Eq, Clone)] + enum TestState { + A, + B, + C, + D, + } + + impl From for (bool, bool) { + fn from(val: TestState) -> Self { + match val { + TestState::A => (false, false), + TestState::B => (true, false), + TestState::C => (false, true), + TestState::D => (true, true), + } + } + } + + impl TryFrom<(bool, bool)> for TestState { + type Error = Infallible; + fn try_from(value: (bool, bool)) -> Result { + Ok(match value { + (false, false) => TestState::A, + (true, false) => TestState::B, + (false, true) => TestState::C, + (true, true) => TestState::D, + }) + } + } + + #[test] + fn capacity_calculation() { + assert_eq!(DoubleBoolArray::<1, TestState>::capacity(), 32); + assert_eq!(DoubleBoolArray::<3, TestState>::capacity(), 96); + } + + #[test] + fn default_initialization() { + let arr = DoubleBoolArray::<2, TestState>::default(); + assert_eq!(arr.find_first_slot_with(TestState::A), Some(0)); + } + + #[test] + fn basic_set_get() { + let mut arr = DoubleBoolArray::<2, TestState>::default(); + + arr.set(0, TestState::B); + assert_eq!(arr.get(0), TestState::B); + + arr.set(31, TestState::C); + assert_eq!(arr.get(31), TestState::C); + + arr.set(63, TestState::D); + assert_eq!(arr.get(63), TestState::D); + } + + #[test] + #[should_panic(expected = "Index out of bounds")] + fn get_out_of_bounds() { + let arr = DoubleBoolArray::<1, TestState>::default(); + arr.get(32); + } + + #[test] + #[should_panic(expected = "Index out of bounds")] + fn set_out_of_bounds() { + let mut arr = DoubleBoolArray::<1, TestState>::default(); + arr.set(32, TestState::A); + } + + #[test] + fn find_empty_slots() { + let mut arr = DoubleBoolArray::<2, TestState>::default(); + + arr.set(5, TestState::B); + assert_eq!(arr.find_first_slot_with(TestState::A), Some(0)); + + arr.set(0, TestState::C); + assert_eq!(arr.find_first_slot_with(TestState::A), Some(1)); + + for i in 0..64 { + arr.set(i, TestState::D); + } + assert_eq!(arr.find_first_slot_with(TestState::A), None); + } + + #[test] + fn slot_independence() { + let mut arr = DoubleBoolArray::<1, TestState>::default(); + + arr.set(0, TestState::B); + arr.set(1, TestState::C); + arr.set(2, TestState::D); + + assert_eq!(arr.get(0), TestState::B); + assert_eq!(arr.get(1), TestState::C); + assert_eq!(arr.get(2), TestState::D); + } + + #[test] + fn all_state_combinations() { + let mut arr = DoubleBoolArray::<1, TestState>::default(); + let states = [TestState::A, TestState::B, TestState::C, TestState::D]; + + for (i, state) in states.iter().enumerate() { + arr.set(i, state.clone()); + assert_eq!(arr.get(i), *state); + } + } +} diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 9936b1e3..c6b33aac 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -1,11 +1,15 @@ +//! This module contains the implementation of the secret service server. +//! This handles networking and communication with clients, but does not implement the traits +//! for the secret service protocol. + pub mod bool_arr; -pub mod ms2sm; +pub mod musig2_session_mgr; use std::{io, marker::Sync, net::SocketAddr, sync::Arc}; use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid}; -use ms2sm::Musig2SessionManager; use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; +use musig2_session_mgr::Musig2SessionManager; pub use quinn::rustls; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, @@ -30,12 +34,18 @@ use terrors::OneOf; use tokio::{sync::Mutex, task::JoinHandle}; use tracing::{error, span, warn, Instrument, Level}; +/// Configuration for the secret service server. +#[derive(Debug)] pub struct Config { + /// The address to bind the server to. pub addr: SocketAddr, + /// The maximum number of concurrent connections allowed. pub connection_limit: Option, + /// The TLS configuration for the server. pub tls_config: rustls::ServerConfig, } +/// Run the secret service server given the service and a server configuration. pub async fn run_server( c: Config, service: Arc, @@ -437,7 +447,7 @@ where stake_index, } => { let preimg = service - .stake_chain() + .stake_chain_preimages() .get_preimg( Txid::from_slice(prestake_txid).expect("correct length"), prestake_vout.into(), diff --git a/crates/secret-service-server/src/ms2sm.rs b/crates/secret-service-server/src/musig2_session_mgr.rs similarity index 80% rename from crates/secret-service-server/src/ms2sm.rs rename to crates/secret-service-server/src/musig2_session_mgr.rs index ccd12225..66db018b 100644 --- a/crates/secret-service-server/src/ms2sm.rs +++ b/crates/secret-service-server/src/musig2_session_mgr.rs @@ -1,3 +1,7 @@ +//! This module contains the Musig2SessionManager which manages in-memory Musig2 +//! sessions globally for a given server. This allows ergonomic (and correct) usage +//! of S2's musig2 features. + use std::{mem::MaybeUninit, sync::Arc}; use musig2::{errors::RoundFinalizeError, LiftedSignature}; @@ -7,6 +11,9 @@ use tokio::sync::{Mutex, MutexGuard}; use crate::bool_arr::DoubleBoolArray; +/// Musig2SessionManager is responsible for tracking and managing secret service +/// musig2 sessions. +#[derive(Debug)] pub struct Musig2SessionManager where SecondRound: Musig2SignerSecondRound, @@ -40,18 +47,32 @@ where } } +/// The provided session index is out of range #[derive(Debug)] pub struct OutOfRange; +/// The session manager is full and cannot accept any more sessions +#[derive(Debug)] +pub struct Full; + +/// The session was assumed to be in a round that it was not in #[derive(Debug)] pub struct NotInCorrectRound { + /// The state the session was assumed to be in pub wanted: SlotState, + /// The state the session was actually in pub got: SlotState, } +/// We couldn't take ownership of the session because something else was still +/// using it. Try again. #[derive(Debug)] pub struct OtherReferencesActive; +/// A struct representing a permission from the session manager to write to a given slot. +/// This allows inspection of the allocated session ID and value before it is transferred +/// to the session manager's ownership. +#[derive(Debug)] pub struct WritePermission<'a, T> { slot: &'a mut MaybeUninit>>, session_id: usize, @@ -59,10 +80,12 @@ pub struct WritePermission<'a, T> { } impl WritePermission<'_, T> { + /// Returns a reference to the value inside the mutex. pub async fn value(&self) -> MutexGuard<'_, T> { self.t.lock().await } + /// Returns the session ID allocated by the session manager. pub fn session_id(&self) -> usize { self.session_id } @@ -79,11 +102,15 @@ where SecondRound: Musig2SignerSecondRound, FirstRound: Musig2SignerFirstRound, { + /// Requests a new session ID from the session manager for a given first round. pub fn new_session( &mut self, first_round: FirstRound, - ) -> Result, OutOfRange> { - let next_empty = self.tracker.find_next_empty_slot().ok_or(OutOfRange)?; + ) -> Result, Full> { + let next_empty = self + .tracker + .find_first_slot_with(SlotState::Empty) + .ok_or(Full)?; let slot = if next_empty <= self.first_rounds.len() { // we're replacing an existing session self.first_rounds.get_mut(next_empty).unwrap() @@ -107,6 +134,8 @@ where } } + /// Attempts to transition a musig2 session from the first round by + /// finalizing it. pub async fn transition_first_to_second_round( &mut self, session_id: usize, @@ -152,6 +181,7 @@ where } } + /// Attempts to finalize the second round of a musig2 session. pub async fn finalize_second_round( &mut self, session_id: usize, @@ -194,6 +224,7 @@ where } } + /// Attempts to retrieve the first round of a musig2 session. pub fn first_round( &self, session_id: usize, @@ -207,6 +238,7 @@ where } } + /// Attempts to retrieve the second round of a musig2 session. pub fn second_round( &self, session_id: usize, @@ -221,10 +253,15 @@ where } } +/// Represents the state of a slot in the musig2 session manager. Used with the +/// bool_arr to improve scan performance. #[derive(Debug)] pub enum SlotState { + /// There's no musig2 session in this slot. Empty, + /// There's a musig2 session in this slot in its first round stage. FirstRound, + /// There's a musig2 session in this slot in its second round stage. SecondRound, } diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index 58ee2ca7..1b40191a 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -5,25 +5,31 @@ edition = "2021" [dependencies] bitcoin.workspace = true -blake3.workspace = true colored = "3.0.0" hkdf = "0.12.4" +make_buf = { git = "https://github.com/alpenlabs/make_buf", version = "1.0.0" } musig2.workspace = true -parking_lot.workspace = true rand.workspace = true rcgen = "0.13.2" -rkyv.workspace = true rustls-pemfile = "2.2.0" secret-service-proto = { version = "0.1.0", path = "../secret-service-proto" } secret-service-server = { version = "0.1.0", path = "../secret-service-server" } serde.workspace = true sha2.workspace = true -sled = "0.34.7" strata-bridge-primitives.workspace = true strata-key-derivation = { git = "https://github.com/alpenlabs/strata", version = "0.1.0" } -terrors.workspace = true tokio.workspace = true toml = "0.8.19" tracing.workspace = true tracing-subscriber.workspace = true + +[lints] +rust.missing_debug_implementations = "warn" +rust.rust_2018_idioms = { level = "deny", priority = -1 } +rust.unreachable_pub = "warn" +rust.unused_crate_dependencies = "deny" +rust.unused_must_use = "deny" +rust.unsafe_op_in_unsafe_fn = "warn" +rust.missing_docs = "warn" +rustdoc.all = "warn" diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 070c65f1..7a850bb5 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -73,7 +73,7 @@ impl SecretService for Service { type WotsSigner = SeededWotsSigner; - type StakeChain = StakeChain; + type StakeChainPreimages = StakeChain; fn operator_signer(&self) -> Self::OperatorSigner { Operator::new(self.keys.wallet_xpriv().private_key) @@ -91,7 +91,7 @@ impl SecretService for Service { SeededWotsSigner::new(self.keys.base_xpriv()) } - fn stake_chain(&self) -> Self::StakeChain { + fn stake_chain_preimages(&self) -> Self::StakeChainPreimages { StakeChain::new(self.keys.base_xpriv()) } } diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index f80f434a..f38e5230 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -7,6 +7,7 @@ use bitcoin::{ Txid, }; use hkdf::Hkdf; +use make_buf::make_buf; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::{PublicKey, SecretKey, SECP256K1}, @@ -88,11 +89,9 @@ impl Musig2Signer for Ms2Signer { } let nonce_seed = { - let info = { - let mut buf = [0; 36]; - buf[0..32].copy_from_slice(&input_txid.as_raw_hash().to_byte_array()); - buf[32..36].copy_from_slice(&input_vout.to_le_bytes()); - buf + let info = make_buf! { + (&input_txid.as_raw_hash().to_byte_array(), 32), + (&input_vout.to_le_bytes(), 4) }; let hk = Hkdf::::new(None, &self.ikm); let mut okm = [0u8; 32]; diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs index 7bb3d0f1..ba66698a 100644 --- a/crates/secret-service/src/disk/stakechain.rs +++ b/crates/secret-service/src/disk/stakechain.rs @@ -6,6 +6,7 @@ use bitcoin::{ Txid, }; use hkdf::Hkdf; +use make_buf::make_buf; use musig2::secp256k1::SECP256K1; use secret_service_proto::v1::traits::{Server, StakeChainPreimages}; use sha2::Sha256; @@ -41,12 +42,10 @@ impl StakeChainPreimages for StakeChain { async move { let hk = Hkdf::::new(None, &self.ikm); let mut okm = [0u8; 32]; - let info = { - let mut buf = [0; 40]; - buf[..32].copy_from_slice(&prestake_txid.as_raw_hash().to_byte_array()); - buf[32..36].copy_from_slice(&prestake_vout.to_le_bytes()); - buf[36..].copy_from_slice(&stake_index.to_le_bytes()); - buf + let info = make_buf! { + (prestake_txid.as_raw_hash().as_byte_array(), 32), + (&prestake_vout.to_le_bytes(), 4), + (&stake_index.to_le_bytes(), 4) }; hk.expand(&info, &mut okm) .expect("32 is a valid length for Sha256 to output"); diff --git a/crates/secret-service/src/disk/wots.rs b/crates/secret-service/src/disk/wots.rs index e2d0e8c2..23a8cc55 100644 --- a/crates/secret-service/src/disk/wots.rs +++ b/crates/secret-service/src/disk/wots.rs @@ -6,16 +6,22 @@ use bitcoin::{ Txid, }; use hkdf::Hkdf; +use make_buf::make_buf; use musig2::secp256k1::SECP256K1; use secret_service_proto::v1::traits::{Server, WotsSigner}; use sha2::Sha256; +/// A Winternitz One-Time Signature (WOTS) key generator seeded with some initial key material. +#[derive(Debug)] pub struct SeededWotsSigner { + /// Initial key material for 160-bit WOTS keys. ikm_160: [u8; 32], + /// Initial key material for 256-bit WOTS keys. ikm_256: [u8; 32], } impl SeededWotsSigner { + /// Creates a new WOTS signer from an operator's base private key (m/20000'). pub fn new(base: &Xpriv) -> Self { Self { ikm_160: base @@ -56,12 +62,10 @@ impl WotsSigner for SeededWotsSigner { async move { let hk = Hkdf::::new(None, &self.ikm_160); let mut okm = [0u8; 20 * 160]; - let info = { - let mut buf = [0; 40]; - buf[..32].copy_from_slice(txid.as_raw_hash().as_byte_array()); - buf[32..36].copy_from_slice(&vout.to_le_bytes()); - buf[36..].copy_from_slice(&index.to_le_bytes()); - buf + let info = make_buf! { + (txid.as_raw_hash().as_byte_array(), 32), + (&vout.to_le_bytes(), 4), + (&index.to_le_bytes(), 4), }; hk.expand(&info, &mut okm).expect("valid output length"); okm @@ -77,12 +81,10 @@ impl WotsSigner for SeededWotsSigner { async move { let hk = Hkdf::::new(None, &self.ikm_256); let mut okm = [0u8; 20 * 256]; - let info = { - let mut buf = [0; 40]; - buf[..32].copy_from_slice(txid.as_raw_hash().as_byte_array()); - buf[32..36].copy_from_slice(&vout.to_le_bytes()); - buf[36..].copy_from_slice(&index.to_le_bytes()); - buf + let info = make_buf! { + (txid.as_raw_hash().as_byte_array(), 32), + (&vout.to_le_bytes(), 4), + (&index.to_le_bytes(), 4), }; hk.expand(&info, &mut okm).expect("valid output length"); okm From b8dc3fd7182323f0280f6e5848f004e5505f22f9 Mon Sep 17 00:00:00 2001 From: Azz Date: Sat, 22 Feb 2025 18:02:11 +0000 Subject: [PATCH 19/30] more docs & tests --- crates/secret-service-client/src/lib.rs | 482 ++---------------- crates/secret-service-client/src/musig2.rs | 289 +++++++++++ crates/secret-service-client/src/operator.rs | 55 ++ crates/secret-service-client/src/p2p.rs | 51 ++ .../secret-service-client/src/stakechain.rs | 45 ++ crates/secret-service-client/src/wots.rs | 65 +++ crates/secret-service-proto/src/v1/mod.rs | 2 + crates/secret-service-proto/src/v1/traits.rs | 132 ++++- crates/secret-service-proto/src/v1/wire.rs | 116 ++++- crates/secret-service-proto/src/wire.rs | 1 + crates/secret-service-server/src/lib.rs | 10 +- 11 files changed, 782 insertions(+), 466 deletions(-) create mode 100644 crates/secret-service-client/src/musig2.rs create mode 100644 crates/secret-service-client/src/operator.rs create mode 100644 crates/secret-service-client/src/p2p.rs create mode 100644 crates/secret-service-client/src/stakechain.rs create mode 100644 crates/secret-service-client/src/wots.rs diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 26bc3ffb..716653b5 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -1,17 +1,22 @@ +//! The client crate for the secret service. Provides implementations of the traits that use a QUIC +//! connection and wire protocol defined in the [`secret_service_proto`] crate to connect with a +//! remote secret service. +pub mod musig2; +pub mod operator; +pub mod p2p; +pub mod stakechain; +pub mod wots; + use std::{ - future::Future, io, net::{Ipv4Addr, SocketAddr}, sync::Arc, time::Duration, }; -use bitcoin::{hashes::Hash, Txid}; -use musig2::{ - errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::{schnorr::Signature, Error, PublicKey}, - AggNonce, LiftedSignature, PubNonce, -}; +use musig2::{Musig2Client, Musig2FirstRound, Musig2SecondRound}; +use operator::OperatorClient; +use p2p::P2PClient; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, @@ -19,11 +24,7 @@ use quinn::{ use rkyv::{deserialize, rancor}; use secret_service_proto::{ v1::{ - traits::{ - Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, - Musig2SignerSecondRound, OperatorSigner, Origin, P2PSigner, SecretService, - SignerIdxOutOfBounds, StakeChainPreimages, WotsSigner, - }, + traits::{Client, ClientError, SecretService}, wire::{ClientMessage, ServerMessage}, }, wire::{ @@ -31,27 +32,36 @@ use secret_service_proto::{ WireMessage, }, }; -use strata_bridge_primitives::scripts::taproot::TaprootWitness; +use stakechain::StakeChainPreimgClient; use terrors::OneOf; use tokio::time::timeout; +use wots::WotsClient; -#[derive(Clone)] +/// Configuration for the S2 client +#[derive(Clone, Debug)] pub struct Config { + /// Server to connect to server_addr: SocketAddr, + /// Hostname present on the server's certificate server_hostname: String, + /// Optional local socket to connect via local_addr: Option, + /// Config for TLS. Note that you should be verifying the server's identity via this to prevent + /// MITM attacks. tls_config: rustls::ClientConfig, + /// Timeout for requests timeout: Duration, } -#[derive(Clone)] +/// A client that connects to a remote secret service via QUIC +#[derive(Clone, Debug)] pub struct SecretServiceClient { - endpoint: Endpoint, config: Arc, conn: Connection, } impl SecretServiceClient { + /// Create a new client and attempt to connect to the server. pub async fn new( config: Config, ) -> Result< @@ -82,7 +92,6 @@ impl SecretServiceClient { let conn = connecting.await.map_err(OneOf::new)?; Ok(SecretServiceClient { - endpoint, config: Arc::new(config), conn, }) @@ -98,454 +107,31 @@ impl SecretService for SecretServic type WotsSigner = WotsClient; - type StakeChainPreimages = StakeChainClient; + type StakeChainPreimages = StakeChainPreimgClient; fn operator_signer(&self) -> Self::OperatorSigner { - OperatorClient { - conn: self.conn.clone(), - config: self.config.clone(), - } + OperatorClient::new(self.conn.clone(), self.config.clone()) } fn p2p_signer(&self) -> Self::P2PSigner { - P2PClient { - conn: self.conn.clone(), - config: self.config.clone(), - } + P2PClient::new(self.conn.clone(), self.config.clone()) } fn musig2_signer(&self) -> Self::Musig2Signer { - Musig2Client { - conn: self.conn.clone(), - config: self.config.clone(), - } + Musig2Client::new(self.conn.clone(), self.config.clone()) } fn wots_signer(&self) -> Self::WotsSigner { - WotsClient { - conn: self.conn.clone(), - config: self.config.clone(), - } + WotsClient::new(self.conn.clone(), self.config.clone()) } fn stake_chain_preimages(&self) -> Self::StakeChainPreimages { - StakeChainClient { - conn: self.conn.clone(), - config: self.config.clone(), - } - } -} - -#[derive(Clone)] -struct Musig2FirstRound { - session_id: Musig2SessionId, - connection: Connection, - config: Arc, -} - -impl Musig2SignerFirstRound for Musig2FirstRound { - fn our_nonce(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundOurNonce { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { - return Err(ClientError::ProtocolError(res)); - }; - PubNonce::from_bytes(&our_nonce).map_err(|_| ClientError::BadData) - } - } - - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundHoldouts { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { - return Err(ClientError::ProtocolError(res)); - }; - pubkeys - .into_iter() - .map(|pk| PublicKey::from_slice(&pk)) - .collect::, Error>>() - .map_err(|_| ClientError::BadData) - } - } - - fn is_complete(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundIsComplete { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(complete) - } - } - - fn receive_pub_nonce( - &mut self, - pubkey: PublicKey, - pubnonce: PubNonce, - ) -> impl Future::Container>> + Send - { - async move { - let msg = ClientMessage::Musig2FirstRoundReceivePubNonce { - session_id: self.session_id, - pubkey: pubkey.serialize(), - pubnonce: pubnonce.serialize(), - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(maybe_err.map_or(Ok(()), Err)) - } - } - - fn finalize( - self, - hash: [u8; 32], - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundFinalize { - session_id: self.session_id, - hash, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(match maybe_err { - Some(e) => Err(e), - None => Ok(Musig2SecondRound { - session_id: self.session_id, - connection: self.connection, - config: self.config, - }), - }) - } - } -} - -struct Musig2SecondRound { - session_id: Musig2SessionId, - connection: Connection, - config: Arc, -} - -impl Musig2SignerSecondRound for Musig2SecondRound { - fn agg_nonce(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundAggNonce { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { - return Err(ClientError::ProtocolError(res)); - }; - AggNonce::from_bytes(&nonce).map_err(|_| ClientError::BadData) - } - } - - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundHoldouts { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { - return Err(ClientError::ProtocolError(res)); - }; - pubkeys - .into_iter() - .map(|pk| PublicKey::from_slice(&pk)) - .collect::, Error>>() - .map_err(|_| ClientError::BadData) - } - } - - fn our_signature( - &self, - ) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundOurSignature { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { - return Err(ClientError::ProtocolError(res)); - }; - musig2::PartialSignature::from_slice(&sig).map_err(|_| ClientError::BadData) - } - } - - fn is_complete(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundIsComplete { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(complete) - } - } - - fn receive_signature( - &mut self, - pubkey: PublicKey, - signature: musig2::PartialSignature, - ) -> impl Future::Container>> + Send - { - async move { - let msg = ClientMessage::Musig2SecondRoundReceiveSignature { - session_id: self.session_id, - pubkey: pubkey.serialize(), - signature: signature.serialize(), - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(maybe_err.map_or(Ok(()), Err)) - } - } - - fn finalize( - self, - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundFinalize { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundFinalize(res) = res else { - return Err(ClientError::ProtocolError(res)); - }; - let res: Result<_, _> = res.into(); - Ok(match res { - Ok(sig) => { - let sig = - LiftedSignature::from_bytes(&sig).map_err(|_| ClientError::BadData)?; - Ok(sig) - } - Err(e) => Err(e), - }) - } - } -} - -struct OperatorClient { - conn: Connection, - config: Arc, -} - -impl OperatorSigner for OperatorClient { - fn sign( - &self, - digest: &[u8; 32], - ) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::OperatorSign { - digest: digest.clone(), - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - match res { - ServerMessage::OperatorSignPsbt { sig } => { - Signature::from_slice(&sig).map_err(|_| ClientError::BadData) - } - _ => Err(ClientError::ProtocolError(res)), - } - } - } - - fn pubkey(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::OperatorPubkey; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - match res { - ServerMessage::OperatorPubkey { pubkey } => { - PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) - } - _ => Err(ClientError::ProtocolError(res)), - } - } - } -} - -struct P2PClient { - conn: Connection, - config: Arc, -} - -impl P2PSigner for P2PClient { - fn sign( - &self, - digest: &[u8; 32], - ) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::P2PSign { - digest: digest.clone(), - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::SignP2P { sig } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Signature::from_slice(&sig).map_err(|_| ClientError::BadData) - } - } - - fn pubkey(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::P2PPubkey; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::P2PPubkey { pubkey } = res else { - return Err(ClientError::ProtocolError(res)); - }; - PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) - } - } -} - -struct Musig2Client { - conn: Connection, - config: Arc, -} - -impl Musig2Signer for Musig2Client { - fn new_session( - &self, - pubkeys: Vec, - witness: TaprootWitness, - input_txid: Txid, - input_vout: u32, - ) -> impl Future, ClientError>> + Send - { - async move { - let msg = ClientMessage::Musig2NewSession { - pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), - witness: witness.into(), - input_txid: input_txid.to_byte_array(), - input_vout, - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::Musig2NewSession(maybe_session_id) = res else { - return Err(ClientError::ProtocolError(res)); - }; - - Ok(match maybe_session_id { - Ok(session_id) => Ok(Musig2FirstRound { - session_id, - connection: self.conn.clone(), - config: self.config.clone(), - }), - Err(e) => Err(e), - }) - } - } - - fn pubkey(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2Pubkey; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::Musig2Pubkey { pubkey } = res else { - return Err(ClientError::ProtocolError(res)); - }; - - PublicKey::from_slice(&pubkey).map_err(|_| ClientError::ProtocolError(res)) - } - } -} - -struct WotsClient { - conn: Connection, - config: Arc, -} - -impl WotsSigner for WotsClient { - fn get_160_key( - &self, - index: u32, - vout: u32, - txid: Txid, - ) -> impl Future::Container<[u8; 20 * 160]>> + Send { - async move { - let msg = ClientMessage::WotsGet160Key { - index, - vout, - txid: txid.as_raw_hash().to_byte_array(), - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::WotsGet160Key { key } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(key) - } - } - - fn get_256_key( - &self, - index: u32, - vout: u32, - txid: Txid, - ) -> impl Future::Container<[u8; 20 * 256]>> + Send { - async move { - let msg = ClientMessage::WotsGet256Key { - index, - vout, - txid: txid.as_raw_hash().to_byte_array(), - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::WotsGet256Key { key } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(key) - } - } -} - -struct StakeChainClient { - conn: Connection, - config: Arc, -} - -impl StakeChainPreimages for StakeChainClient { - fn get_preimg( - &self, - prestake_txid: Txid, - prestake_vout: u32, - stake_index: u32, - ) -> impl Future::Container<[u8; 32]>> + Send { - async move { - let msg = ClientMessage::StakeChainGetPreimage { - prestake_txid: prestake_txid.to_byte_array(), - prestake_vout, - stake_index, - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::StakeChainGetPreimage { preimg } = res else { - return Err(ClientError::ProtocolError(res)); - }; - Ok(preimg) - } + StakeChainPreimgClient::new(self.conn.clone(), self.config.clone()) } } -async fn make_v1_req( +/// Makes a v1 secret service request via quic +pub async fn make_v1_req( conn: &Connection, msg: ClientMessage, timeout_dur: Duration, diff --git a/crates/secret-service-client/src/musig2.rs b/crates/secret-service-client/src/musig2.rs new file mode 100644 index 00000000..366fe937 --- /dev/null +++ b/crates/secret-service-client/src/musig2.rs @@ -0,0 +1,289 @@ +//! Musig2 signer client +use std::{future::Future, sync::Arc}; + +use bitcoin::{hashes::Hash, Txid}; +use musig2::{ + errors::{RoundContributionError, RoundFinalizeError}, + secp256k1::PublicKey, + AggNonce, LiftedSignature, PubNonce, +}; +use quinn::Connection; +use secret_service_proto::v1::{ + traits::{ + Client, ClientError, Musig2SessionId, Musig2Signer, Musig2SignerFirstRound, + Musig2SignerSecondRound, Origin, SignerIdxOutOfBounds, + }, + wire::{ClientMessage, ServerMessage}, +}; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; + +use crate::{make_v1_req, Config}; + +pub struct Musig2Client { + conn: Connection, + config: Arc, +} + +impl Musig2Client { + pub fn new(conn: Connection, config: Arc) -> Self { + Self { conn, config } + } +} + +impl Musig2Signer for Musig2Client { + fn new_session( + &self, + pubkeys: Vec, + witness: TaprootWitness, + input_txid: Txid, + input_vout: u32, + ) -> impl Future, ClientError>> + Send + { + async move { + let msg = ClientMessage::Musig2NewSession { + pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), + witness: witness.into(), + input_txid: input_txid.to_byte_array(), + input_vout, + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2NewSession(maybe_session_id) = res else { + return Err(ClientError::WrongMessage(res)); + }; + + Ok(match maybe_session_id { + Ok(session_id) => Ok(Musig2FirstRound { + session_id, + connection: self.conn.clone(), + config: self.config.clone(), + }), + Err(e) => Err(e), + }) + } + } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2Pubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2Pubkey { pubkey } = res else { + return Err(ClientError::WrongMessage(res)); + }; + + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res)) + } + } +} + +#[derive(Clone)] +pub struct Musig2FirstRound { + session_id: Musig2SessionId, + connection: Connection, + config: Arc, +} + +impl Musig2SignerFirstRound for Musig2FirstRound { + fn our_nonce(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2FirstRoundOurNonce { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { + return Err(ClientError::WrongMessage(res)); + }; + PubNonce::from_bytes(&our_nonce).map_err(|_| ClientError::BadData) + } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send { + async move { + let msg = ClientMessage::Musig2FirstRoundHoldouts { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { + return Err(ClientError::WrongMessage(res)); + }; + pubkeys + .into_iter() + .map(|pk| PublicKey::from_slice(&pk)) + .collect::, musig2::secp256k1::Error>>() + .map_err(|_| ClientError::BadData) + } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2FirstRoundIsComplete { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(complete) + } + } + + fn receive_pub_nonce( + &mut self, + pubkey: PublicKey, + pubnonce: PubNonce, + ) -> impl Future::Container>> + Send + { + async move { + let msg = ClientMessage::Musig2FirstRoundReceivePubNonce { + session_id: self.session_id, + pubkey: pubkey.serialize(), + pubnonce: pubnonce.serialize(), + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(maybe_err.map_or(Ok(()), Err)) + } + } + + fn finalize( + self, + hash: [u8; 32], + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { + let msg = ClientMessage::Musig2FirstRoundFinalize { + session_id: self.session_id, + digest: hash, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(match maybe_err { + Some(e) => Err(e), + None => Ok(Musig2SecondRound { + session_id: self.session_id, + connection: self.connection, + config: self.config, + }), + }) + } + } +} + +pub struct Musig2SecondRound { + session_id: Musig2SessionId, + connection: Connection, + config: Arc, +} + +impl Musig2SignerSecondRound for Musig2SecondRound { + fn agg_nonce(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2SecondRoundAggNonce { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { + return Err(ClientError::WrongMessage(res)); + }; + AggNonce::from_bytes(&nonce).map_err(|_| ClientError::BadData) + } + } + + fn holdouts( + &self, + ) -> impl Future::Container>> + Send { + async move { + let msg = ClientMessage::Musig2SecondRoundHoldouts { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { + return Err(ClientError::WrongMessage(res)); + }; + pubkeys + .into_iter() + .map(|pk| PublicKey::from_slice(&pk)) + .collect::, musig2::secp256k1::Error>>() + .map_err(|_| ClientError::BadData) + } + } + + fn our_signature( + &self, + ) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2SecondRoundOurSignature { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { + return Err(ClientError::WrongMessage(res)); + }; + musig2::PartialSignature::from_slice(&sig).map_err(|_| ClientError::BadData) + } + } + + fn is_complete(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::Musig2SecondRoundIsComplete { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(complete) + } + } + + fn receive_signature( + &mut self, + pubkey: PublicKey, + signature: musig2::PartialSignature, + ) -> impl Future::Container>> + Send + { + async move { + let msg = ClientMessage::Musig2SecondRoundReceiveSignature { + session_id: self.session_id, + pubkey: pubkey.serialize(), + signature: signature.serialize(), + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(maybe_err.map_or(Ok(()), Err)) + } + } + + fn finalize( + self, + ) -> impl Future< + Output = ::Container>, + > + Send { + async move { + let msg = ClientMessage::Musig2SecondRoundFinalize { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundFinalize(res) = res else { + return Err(ClientError::WrongMessage(res)); + }; + let res: Result<_, _> = res.into(); + Ok(match res { + Ok(sig) => { + let sig = + LiftedSignature::from_bytes(&sig).map_err(|_| ClientError::BadData)?; + Ok(sig) + } + Err(e) => Err(e), + }) + } + } +} diff --git a/crates/secret-service-client/src/operator.rs b/crates/secret-service-client/src/operator.rs new file mode 100644 index 00000000..6bfcfffa --- /dev/null +++ b/crates/secret-service-client/src/operator.rs @@ -0,0 +1,55 @@ +//! Operator signer client +use std::{future::Future, sync::Arc}; + +use musig2::secp256k1::{schnorr::Signature, PublicKey}; +use quinn::Connection; +use secret_service_proto::v1::{ + traits::{Client, ClientError, OperatorSigner, Origin}, + wire::{ClientMessage, ServerMessage}, +}; + +use crate::{make_v1_req, Config}; + +pub struct OperatorClient { + conn: Connection, + config: Arc, +} + +impl OperatorClient { + pub fn new(conn: Connection, config: Arc) -> Self { + Self { conn, config } + } +} + +impl OperatorSigner for OperatorClient { + fn sign( + &self, + digest: &[u8; 32], + ) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::OperatorSign { + digest: digest.clone(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + match res { + ServerMessage::OperatorSign { sig } => { + Signature::from_slice(&sig).map_err(|_| ClientError::BadData) + } + _ => Err(ClientError::WrongMessage(res)), + } + } + } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::OperatorPubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + match res { + ServerMessage::OperatorPubkey { pubkey } => { + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) + } + _ => Err(ClientError::WrongMessage(res)), + } + } + } +} diff --git a/crates/secret-service-client/src/p2p.rs b/crates/secret-service-client/src/p2p.rs new file mode 100644 index 00000000..a7022c8a --- /dev/null +++ b/crates/secret-service-client/src/p2p.rs @@ -0,0 +1,51 @@ +//! P2P signer client +use std::{future::Future, sync::Arc}; + +use musig2::secp256k1::{schnorr::Signature, PublicKey}; +use quinn::Connection; +use secret_service_proto::v1::{ + traits::{Client, ClientError, Origin, P2PSigner}, + wire::{ClientMessage, ServerMessage}, +}; + +use crate::{make_v1_req, Config}; + +pub struct P2PClient { + conn: Connection, + config: Arc, +} + +impl P2PClient { + pub fn new(conn: Connection, config: Arc) -> Self { + Self { conn, config } + } +} + +impl P2PSigner for P2PClient { + fn sign( + &self, + digest: &[u8; 32], + ) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::P2PSign { + digest: digest.clone(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::P2PSign { sig } = res else { + return Err(ClientError::WrongMessage(res)); + }; + Signature::from_slice(&sig).map_err(|_| ClientError::BadData) + } + } + + fn pubkey(&self) -> impl Future::Container> + Send { + async move { + let msg = ClientMessage::P2PPubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::P2PPubkey { pubkey } = res else { + return Err(ClientError::WrongMessage(res)); + }; + PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) + } + } +} diff --git a/crates/secret-service-client/src/stakechain.rs b/crates/secret-service-client/src/stakechain.rs new file mode 100644 index 00000000..5832b50d --- /dev/null +++ b/crates/secret-service-client/src/stakechain.rs @@ -0,0 +1,45 @@ +//! Stakechain preimages client +use std::{future::Future, sync::Arc}; + +use bitcoin::{hashes::Hash, Txid}; +use quinn::Connection; +use secret_service_proto::v1::{ + traits::{Client, ClientError, Origin, StakeChainPreimages}, + wire::{ClientMessage, ServerMessage}, +}; + +use crate::{make_v1_req, Config}; + +pub struct StakeChainPreimgClient { + conn: Connection, + config: Arc, +} + +impl StakeChainPreimgClient { + /// Guess? + pub fn new(conn: Connection, config: Arc) -> Self { + Self { conn, config } + } +} + +impl StakeChainPreimages for StakeChainPreimgClient { + fn get_preimg( + &self, + prestake_txid: Txid, + prestake_vout: u32, + stake_index: u32, + ) -> impl Future::Container<[u8; 32]>> + Send { + async move { + let msg = ClientMessage::StakeChainGetPreimage { + prestake_txid: prestake_txid.to_byte_array(), + prestake_vout, + stake_index, + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::StakeChainGetPreimage { preimg } = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(preimg) + } + } +} diff --git a/crates/secret-service-client/src/wots.rs b/crates/secret-service-client/src/wots.rs new file mode 100644 index 00000000..589c500a --- /dev/null +++ b/crates/secret-service-client/src/wots.rs @@ -0,0 +1,65 @@ +//! WOTS signer client +use std::{future::Future, sync::Arc}; + +use bitcoin::{hashes::Hash, Txid}; +use quinn::Connection; +use secret_service_proto::v1::{ + traits::{Client, ClientError, Origin, WotsSigner}, + wire::{ClientMessage, ServerMessage}, +}; + +use crate::{make_v1_req, Config}; + +pub struct WotsClient { + conn: Connection, + config: Arc, +} + +impl WotsClient { + /// Creates a new wots client with an existing quic connection and config + pub fn new(conn: Connection, config: Arc) -> Self { + Self { conn, config } + } +} + +impl WotsSigner for WotsClient { + fn get_160_key( + &self, + index: u32, + vout: u32, + txid: Txid, + ) -> impl Future::Container<[u8; 20 * 160]>> + Send { + async move { + let msg = ClientMessage::WotsGet160Key { + index, + vout, + txid: txid.as_raw_hash().to_byte_array(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::WotsGet160Key { key } = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(key) + } + } + + fn get_256_key( + &self, + index: u32, + vout: u32, + txid: Txid, + ) -> impl Future::Container<[u8; 20 * 256]>> + Send { + async move { + let msg = ClientMessage::WotsGet256Key { + index, + vout, + txid: txid.as_raw_hash().to_byte_array(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::WotsGet256Key { key } = res else { + return Err(ClientError::WrongMessage(res)); + }; + Ok(key) + } + } +} diff --git a/crates/secret-service-proto/src/v1/mod.rs b/crates/secret-service-proto/src/v1/mod.rs index ecb52929..27374c0c 100644 --- a/crates/secret-service-proto/src/v1/mod.rs +++ b/crates/secret-service-proto/src/v1/mod.rs @@ -1,3 +1,5 @@ +//! V1 secret service +#[allow(missing_docs)] pub mod rkyv_wrappers; pub mod traits; pub mod wire; diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index d37a519a..2ff8134a 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -1,3 +1,5 @@ +//! The traits that make up the secret service's interfaces + use std::future::Future; use bitcoin::Txid; @@ -22,38 +24,78 @@ where O: Origin, FirstRound: Musig2SignerFirstRound, { + /// Implementation of the OperatorSigner trait. type OperatorSigner: OperatorSigner; + + /// Implementation of the P2PSigner trait. type P2PSigner: P2PSigner; + + /// Implementation of the Musig2Signer trait. type Musig2Signer: Musig2Signer; + + /// Implementation of the WotsSigner trait. type WotsSigner: WotsSigner; + + /// Implementation of the StakeChainPreimages trait. type StakeChainPreimages: StakeChainPreimages; + /// Creates an instance of the OperatorSigner. fn operator_signer(&self) -> Self::OperatorSigner; + + /// Creates an instance of the P2PSigner. fn p2p_signer(&self) -> Self::P2PSigner; + + /// Creates an instance of the Musig2Signer. fn musig2_signer(&self) -> Self::Musig2Signer; + + /// Creates an instance of the WotsSigner. fn wots_signer(&self) -> Self::WotsSigner; + + /// Creates an instance of the StakeChainPreimages. fn stake_chain_preimages(&self) -> Self::StakeChainPreimages; } +/// The operator signer signs transactions for the operator's own wallet that +/// is used for fronting withdrawals and other operations. +/// +/// This should have its own unique key that isn't used for any other purpose. pub trait OperatorSigner: Send { + /// Signs a digest using the operator's private key. fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; + /// Returns the public key of the operator's secret key. fn pubkey(&self) -> impl Future> + Send; } +/// The P2P signer is used for signing messages between operators on the peer-to-peer network. +/// +/// This should have its own unique key that isn't used for any other purpose. pub trait P2PSigner: Send { + /// Signs a digest using the operator's private key. fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; + /// Returns the public key of the operator's secret key. fn pubkey(&self) -> impl Future> + Send; } +/// Uniquely identifies an in-memory musig2 session on the signing server. pub type Musig2SessionId = usize; +/// Error returned when trying to access a signer that is out of bounds. #[derive(Debug, Archive, Serialize, Deserialize, Clone)] pub struct SignerIdxOutOfBounds { + /// Index we were trying to access. pub index: usize, + /// Number of signers in the session. pub n_signers: usize, } +/// The musig2 signer trait is used to bootstrap and begin a musig2 session. +/// A single secret key should be used across all sessions initiated by this signer, +/// whose public key should be accessible via the `pubkey` method. pub trait Musig2Signer: Send + Sync { + /// Initialize a new musig2 session with the given public keys, witness, input transaction ID, + /// and input vout. `pubkeys` may or may not include our own pubkey and should be added if not + /// included by implementor. `pubkeys` may or may not be sorted, so should be sorted + /// determistically (after addition of our own pubkey if required) before session creation fn new_session( &self, pubkeys: Vec, @@ -61,49 +103,104 @@ pub trait Musig2Signer: Send + Sync { input_txid: Txid, input_vout: u32, ) -> impl Future>> + Send; + /// Retrieve the public key associated with this musig2 signer. fn pubkey(&self) -> impl Future> + Send; } +/// Represents a state-machine-like API for performing musig2 signing. This first round is returned +/// by the `new_session` method of the `Musig2Signer` trait. +/// +/// This enables ergonomic usage of the (relatively) complex musig2 signing process via generics. +/// The secret-service-client crate provides a client-side implementation of this trait, and +/// implementors should provide their own implementation server-side. pub trait Musig2SignerFirstRound: Send + Sync { + /// Returns our public nonce which should be shared with other signers. fn our_nonce(&self) -> impl Future> + Send; + /// Returns a vector of all signer public keys who we have yet to receive a [`PubNonce`] from. + /// Note that this will never return our own public key. fn holdouts(&self) -> impl Future>> + Send; + /// Returns true once all public nonces have been received from every signer. fn is_complete(&self) -> impl Future> + Send; + /// Adds a [`PubNonce`] to the internal state, registering it to a specific signer at a given + /// index. Returns an error if the signer index is out of range, or if we already have a + /// different nonce on-file for that signer. fn receive_pub_nonce( &mut self, pubkey: PublicKey, pubnonce: PubNonce, ) -> impl Future>> + Send; + /// Finishes the first round once all nonces are received, combining nonces + /// into an aggregated nonce, and creating a partial signature using `seckey` + /// on a given `message`, both of which are stored in the returned `SecondRound`. + /// + /// This method intentionally consumes the `FirstRound`, to avoid accidentally + /// reusing a secret-nonce. + /// + /// This method should only be invoked once [`is_complete`][Self::is_complete] + /// returns true, otherwise it will fail. Can also return an error if partial + /// signing fails, probably because the wrong secret key was given. + /// + /// For all partial signatures to be valid, everyone must naturally be signing the + /// same message. fn finalize( self, - hash: [u8; 32], + digest: [u8; 32], ) -> impl Future>> + Send; } +/// This trait represents the second round of the musig2 signing process. +/// It is responsible for aggregating the partial signatures into a single +/// signature, and for verifying the aggregated signature. pub trait Musig2SignerSecondRound: Send + Sync { + /// Returns the aggregated nonce built from the nonces provided in the first round. Signers who + /// find themselves in an aggregator role can distribute this aggregated nonce to other signers + /// to that they can produce an aggregated signature without 1:1 communication between every + /// pair of signers. fn agg_nonce(&self) -> impl Future> + Send; + /// Returns a vector of signer public keys from whom we have yet to receive a + /// [`PartialSignature`]. Note that since our signature was constructed at the end of the + /// first round, this vector will never contain our own public key. fn holdouts(&self) -> impl Future>> + Send; + /// Returns the partial signature created during finalization of the first round. fn our_signature(&self) -> impl Future> + Send; + /// Returns true once we have all partial signatures from the group. fn is_complete(&self) -> impl Future> + Send; + /// Adds a [`PartialSignature`] to the internal state, registering it to a specific signer. + /// Returns an error if the signature is not valid, or if the given public key isn't part of + /// the set of signers, or if we already have a different partial signature on-file for that + /// signer. fn receive_signature( &mut self, pubkey: PublicKey, signature: PartialSignature, ) -> impl Future>> + Send; + /// Finishes the second round once all partial signatures are received, + /// combining signatures into an aggregated signature on the `message` + /// given in the first round finalization. + /// + /// This method should only be invoked once [`is_complete`][Self::is_complete] + /// returns true, otherwise it will fail. Can also return an error if partial + /// signature aggregation fails, but if [`receive_signature`][Self::receive_signature] + /// didn't complain, then finalizing will succeed with overwhelming probability. fn finalize( self, ) -> impl Future>> + Send; } +/// Winternitz One-Time Signatures are used to transfer state across UTXOs, even though +/// bitcoin does not support this natively. This signer returns deterministic keys so the +/// caller can assemble a transaction. pub trait WotsSigner: Send { + /// Returns a deterministic key usable for signing 160 bits of data, with 20 bytes per bit. fn get_160_key( &self, index: u32, @@ -111,6 +208,7 @@ pub trait WotsSigner: Send { txid: Txid, ) -> impl Future> + Send; + /// Returns a key usable for signing 256 bits of data, with 20 bytes per bit. fn get_256_key( &self, index: u32, @@ -119,7 +217,11 @@ pub trait WotsSigner: Send { ) -> impl Future> + Send; } +/// The stakechain preimages struct is used to generate deterministic preimages for the stakechain +/// used for withdrawals. pub trait StakeChainPreimages: Send { + /// Returns a deterministic preimage for a given stakechain withdrawal through a given txid, + /// vout and stake index. fn get_preimg( &self, prestake_txid: Txid, @@ -128,29 +230,53 @@ pub trait StakeChainPreimages: Send { ) -> impl Future> + Send; } +/// The origin trait is used to parameterize the main secret service traits so +/// that clients and servers alike can implement a single trait, but clients +/// will receive the server's response wrapped in a result with other spurious +/// network or protocol errors it may encounter. pub trait Origin { + /// Container type for responses from secret service traits type Container; } -/// Enforcer for other traits to ensure implementations only work for either the client or server +/// Enforcer for other traits to ensure implementations only work for the server & provides +/// container type +#[derive(Debug)] pub struct Server; impl Origin for Server { + // for the server, this is just a transparent wrapper type Container = T; } +/// Enforcer for other traits to ensure implementations only work for the client & provides +/// container type +#[derive(Debug)] pub struct Client; impl Origin for Client { + // for the client, we wrap responses in a result that may have a client error type Container = Result; } +/// Various errors a client may encounter when interacting with the secret service +#[derive(Debug)] pub enum ClientError { + /// Connection was lost or had an error ConnectionError(ConnectionError), + /// Unusual: rkyv failed to serialize something. Indicates something very bad has happened. SerializationError(rancor::Error), + /// rkyv failed to deserialize something. Something's probably weird on the + /// server side DeserializationError(rancor::Error), + /// We failed to deserialize something. Server is giving us bad responses BadData, + /// We failed to write data towards the server WriteError(WriteError), + /// We failed to read data from the server ReadError(ReadExactError), + /// The server took too long to respond Timeout, - ProtocolError(ServerMessage), + /// The server sent a message that was not expected + WrongMessage(ServerMessage), + /// The server sent a message with an unexpected protocol version WrongVersion, } diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 3d659188..e25d1570 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -1,3 +1,4 @@ +//! V1 wire protocol use bitcoin::{ hashes::Hash, taproot::{ControlBlock, TaprootError}, @@ -9,77 +10,121 @@ use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::traits::{Musig2SessionId, SignerIdxOutOfBounds}; +/// Various messages the server can send to the client. #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum ServerMessage { + /// The message the client sent was invalid. InvalidClientMessage, + /// The server experienced an unexpected internal error while handling the + /// request. Check the server logs for debugging details. OpaqueServerError, - OperatorSignPsbt { + /// Response for OperatorSigner::sign + OperatorSign { + /// Schnorr signature of provided digest sig: [u8; 64], }, + /// Response for OperatorSigner::pubkey OperatorPubkey { + /// Serialized Schnorr compressed public key for operator signatures pubkey: [u8; 33], }, - SignP2P { + /// Response for P2PSigner::sign + P2PSign { + /// Schnorr signature of provided digest sig: [u8; 64], }, + /// Response for P2PSigner::pubkey P2PPubkey { + /// Serialized Schnorr compressed public key for P2P signatures pubkey: [u8; 33], }, + /// Response for Musig2Signer::new_session Musig2NewSession(Result), + /// Response for Musig2Signer::pubkey Musig2Pubkey { + /// Serialized Schnorr compressed public key for Musig2 signatures pubkey: [u8; 33], }, + /// Response for Musig2SignerFirstRound::our_nonce Musig2FirstRoundOurNonce { + /// Our serialized musig2 public nonce for the requested signing session our_nonce: [u8; 66], }, + /// Response for Musig2SignerFirstRound::holdouts Musig2FirstRoundHoldouts { + /// Serialized Schnorr compressed public keys of signers whose pub nonces + /// we do not have pubkeys: Vec<[u8; 33]>, }, + /// Response for Musig2SignerFirstRound::is_complete Musig2FirstRoundIsComplete { + /// What do you think it means? complete: bool, }, + /// Response for Musig2SignerFirstRound::receive_pub_nonce Musig2FirstRoundReceivePubNonce( #[rkyv(with = Map)] Option, ), + /// Response for Musig2SignerFirstRound::finalize Musig2FirstRoundFinalize( #[rkyv(with = Map)] Option, ), + /// Response for Musig2SignerSecondRound::agg_nonce Musig2SecondRoundAggNonce { + /// Serialized aggregated nonce of the signing session's first round nonce: [u8; 66], }, + /// Response for Musig2SignerSecondRound::holdouts Musig2SecondRoundHoldouts { + /// Serialized Schnorr compressed public keys of signers whose partial signatures + /// we do not have for this signing session pubkeys: Vec<[u8; 33]>, }, + /// Response for Musig2SignerSecondRound::our_signature Musig2SecondRoundOurSignature { + /// Our serialized partial signature of the signing session sig: [u8; 32], }, + /// Response for Musig2SignerSecondRound::is_complete Musig2SecondRoundIsComplete { + /// Hmm. I wonder what this could mean. complete: bool, }, + /// Response for Musig2SignerSecondRound::receive_signature Musig2SecondRoundReceiveSignature( #[rkyv(with = Map)] Option, ), + /// Response for Musig2SignerSecondRound::finalize Musig2SecondRoundFinalize(Musig2SessionResult), + /// Response for WotsSigner::get_160_key WotsGet160Key { + /// A set of 20 byte keys, one for each bit key: [u8; 20 * 160], }, + /// Response for WotsSigner::get_256_key WotsGet256Key { + /// A set of 20 byte keys, one for each bit key: [u8; 20 * 256], }, + /// Response for StakeChainPreimages::get_preimg StakeChainGetPreimage { + /// The preimage you asked for? preimg: [u8; 32], }, } +/// Helper type for serialization +/// Maybe replaced with a future rkyv::with::MapRes or smth? +#[allow(missing_docs)] #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum Musig2SessionResult { Ok([u8; 64]), @@ -106,84 +151,139 @@ impl From for Result<[u8; 64], RoundFinalizeError> { // impl> WireMessageMarker for ServerMessage {} +/// Various messages the client can send to the server. #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum ClientMessage { + /// Request for OperatorSigner::sign OperatorSign { + /// The digest of the data we want signed digest: [u8; 32], }, + /// Request for OperatorSigner::pubkey OperatorPubkey, + /// Request for P2PSigner::sign P2PSign { + /// The digest of the data we want signed digest: [u8; 32], }, + /// Request for P2PSigner::pubkey P2PPubkey, + /// Request for Musig2Signer::new_session Musig2NewSession { + /// Public keys for the signing session. May or may not include our own + /// public key. If not present, it should be added. May or may not be sorted. pubkeys: Vec<[u8; 33]>, + /// The taproot witness of the input witness: SerializableTaprootWitness, + /// Serialized txid of the input tx input_txid: [u8; 32], + /// The vout of the input tx we're signing for (i think?) input_vout: u32, }, + /// Request for Musig2Signer::pubkey Musig2Pubkey, + /// Request for Musig2SignerFirstRound::our_nonce Musig2FirstRoundOurNonce { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerFirstRound::holdouts Musig2FirstRoundHoldouts { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerFirstRound::is_complete Musig2FirstRoundIsComplete { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerFirstRound::receive_pub_nonce Musig2FirstRoundReceivePubNonce { + /// Session that we're requesting for session_id: usize, + /// The serialized compressed schnorr pubkey of the signer whose pubnonce this is pubkey: [u8; 33], + /// Serialized public nonce pubnonce: [u8; 66], }, + /// Request for Musig2SignerFirstRound::finalize Musig2FirstRoundFinalize { + /// Session that we're requesting for session_id: usize, - hash: [u8; 32], + /// Digest of message we're signing + digest: [u8; 32], }, + /// Request for Musig2SignerSecondRound::agg_nonce Musig2SecondRoundAggNonce { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerSecondRound::holdouts Musig2SecondRoundHoldouts { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerSecondRound::our_signature Musig2SecondRoundOurSignature { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerSecondRound::is_complete Musig2SecondRoundIsComplete { + /// Session that we're requesting for session_id: usize, }, + /// Request for Musig2SignerSecondRound::receive_signature Musig2SecondRoundReceiveSignature { + /// Session that we're requesting for session_id: usize, + /// The serialized compressed schnorr pubkey of the signer whose pubnonce this is pubkey: [u8; 33], + /// That signer's musig2 partial sig signature: [u8; 32], }, + /// Request for Musig2SignerSecondRound::finalize Musig2SecondRoundFinalize { + /// Session that we're requesting for session_id: usize, }, + /// Request for WotsSigner::get_160_key WotsGet160Key { + /// Transaction index (?) opaque index: u32, + /// Transaction vout (?) opaque vout: u32, + /// Transaction txid (?) opaque txid: [u8; 32], }, + /// Request for WotsSigner::get_256_key WotsGet256Key { + /// Transaction index (?) opaque index: u32, + /// Transaction vout (?) opaque vout: u32, + /// Transaction txid (?) opaque txid: [u8; 32], }, + /// Request for StakeChainPreimages::get_preimg StakeChainGetPreimage { + /// Transaction txid (?) opaque prestake_txid: [u8; 32], + /// Transaction vout (?) opaque prestake_vout: u32, + /// Stake index (?) opaque stake_index: u32, }, } +/// Serializable version of [`TaprootWitness`] +#[allow(missing_docs)] #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum SerializableTaprootWitness { Key, @@ -214,13 +314,8 @@ impl From for SerializableTaprootWitness { } } -pub enum TaprootWitnessError { - InvalidWitnessType, - InvalidScriptControlBlock(TaprootError), -} - impl TryFrom for TaprootWitness { - type Error = TaprootWitnessError; + type Error = TaprootError; fn try_from(value: SerializableTaprootWitness) -> Result { match value { SerializableTaprootWitness::Key => Ok(TaprootWitness::Key), @@ -229,8 +324,7 @@ impl TryFrom for TaprootWitness { control_block, } => { let script_buf = ScriptBuf::from_bytes(script_buf); - let control_block = ControlBlock::decode(&control_block) - .map_err(TaprootWitnessError::InvalidScriptControlBlock)?; + let control_block = ControlBlock::decode(&control_block)?; Ok(TaprootWitness::Script { script_buf, control_block, diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs index b7f445cf..a2fd3f44 100644 --- a/crates/secret-service-proto/src/wire.rs +++ b/crates/secret-service-proto/src/wire.rs @@ -1,3 +1,4 @@ +//! Secret service wire protocol use rkyv::{ api::high::{to_bytes_in, HighSerializer}, rancor, diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index c6b33aac..34913a48 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -179,7 +179,7 @@ where ArchivedVersionedClientMessage::V1(req) => match req { ArchivedClientMessage::OperatorSign { digest } => { let sig = service.operator_signer().sign(digest).await; - ServerMessage::OperatorSignPsbt { + ServerMessage::OperatorSign { sig: sig.serialize(), } } @@ -193,7 +193,7 @@ where ArchivedClientMessage::P2PSign { digest } => { let sig = service.p2p_signer().sign(digest).await; - ServerMessage::SignP2P { + ServerMessage::P2PSign { sig: sig.serialize(), } } @@ -314,10 +314,12 @@ where _ => ServerMessage::InvalidClientMessage, } } - ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, hash } => { + ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, digest } => { let session_id = session_id.to_native() as usize; let mut sm = musig2_sm.lock().await; - let r = sm.transition_first_to_second_round(session_id, *hash).await; + let r = sm + .transition_first_to_second_round(session_id, *digest) + .await; if let Err(e) = r { use terrors::E3::*; From 1598ee6f76bca953246d4cf5f47ed8c86de2b35c Mon Sep 17 00:00:00 2001 From: Azz Date: Sun, 23 Feb 2025 16:48:46 +0000 Subject: [PATCH 20/30] e2e tests, xonlypublickeys --- Cargo.lock | 1 + crates/secret-service-client/src/lib.rs | 38 ++--- crates/secret-service-client/src/musig2.rs | 24 ++-- crates/secret-service-client/src/operator.rs | 5 +- crates/secret-service-client/src/p2p.rs | 5 +- crates/secret-service-proto/src/v1/traits.rs | 18 +-- crates/secret-service-proto/src/v1/wire.rs | 16 +-- crates/secret-service-proto/src/wire.rs | 19 +-- crates/secret-service-server/src/lib.rs | 27 ++-- crates/secret-service/Cargo.toml | 4 + crates/secret-service/src/disk/mod.rs | 7 +- crates/secret-service/src/disk/musig2.rs | 35 +++-- crates/secret-service/src/disk/operator.rs | 6 +- crates/secret-service/src/disk/p2p.rs | 11 +- crates/secret-service/src/main.rs | 2 + crates/secret-service/src/tests.rs | 138 +++++++++++++++++++ 16 files changed, 251 insertions(+), 105 deletions(-) create mode 100644 crates/secret-service/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 1dceb948..f2030f10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6861,6 +6861,7 @@ dependencies = [ "rand", "rcgen", "rustls-pemfile", + "secret-service-client", "secret-service-proto", "secret-service-server", "serde", diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 716653b5..d950ebfc 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -5,6 +5,7 @@ pub mod musig2; pub mod operator; pub mod p2p; pub mod stakechain; + pub mod wots; use std::{ @@ -21,7 +22,7 @@ use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicClientConfig}, rustls, ClientConfig, ConnectError, Connection, ConnectionError, Endpoint, }; -use rkyv::{deserialize, rancor}; +use rkyv::{deserialize, rancor, util::AlignedVec}; use secret_service_proto::{ v1::{ traits::{Client, ClientError, SecretService}, @@ -41,16 +42,16 @@ use wots::WotsClient; #[derive(Clone, Debug)] pub struct Config { /// Server to connect to - server_addr: SocketAddr, + pub server_addr: SocketAddr, /// Hostname present on the server's certificate - server_hostname: String, + pub server_hostname: String, /// Optional local socket to connect via - local_addr: Option, + pub local_addr: Option, /// Config for TLS. Note that you should be verifying the server's identity via this to prevent /// MITM attacks. - tls_config: rustls::ClientConfig, + pub tls_config: rustls::ClientConfig, /// Timeout for requests - timeout: Duration, + pub timeout: Duration, } /// A client that connects to a remote secret service via QUIC @@ -137,17 +138,17 @@ pub async fn make_v1_req( timeout_dur: Duration, ) -> Result { let (mut tx, mut rx) = conn.open_bi().await.map_err(ClientError::ConnectionError)?; - timeout( - timeout_dur, - tx.write_all( - &VersionedClientMessage::V1(msg) - .serialize() - .map_err(ClientError::SerializationError)?, - ), - ) - .await - .map_err(|_| ClientError::Timeout)? - .map_err(ClientError::WriteError)?; + let (len_bytes, msg_bytes) = VersionedClientMessage::V1(msg) + .serialize() + .map_err(ClientError::SerializationError)?; + timeout(timeout_dur, tx.write_all(&len_bytes)) + .await + .map_err(|_| ClientError::Timeout)? + .map_err(ClientError::WriteError)?; + timeout(timeout_dur, tx.write_all(&msg_bytes)) + .await + .map_err(|_| ClientError::Timeout)? + .map_err(ClientError::WriteError)?; let len_to_read = { let mut buf = [0; size_of::()]; @@ -158,7 +159,8 @@ pub async fn make_v1_req( LengthUint::from_le_bytes(buf) }; - let mut buf = vec![0; len_to_read as usize]; + let mut buf: AlignedVec<16> = AlignedVec::with_capacity(len_to_read as usize); + buf.resize(len_to_read as usize, 0); timeout(timeout_dur, rx.read_exact(&mut buf)) .await .map_err(|_| ClientError::Timeout)? diff --git a/crates/secret-service-client/src/musig2.rs b/crates/secret-service-client/src/musig2.rs index 366fe937..95e1115f 100644 --- a/crates/secret-service-client/src/musig2.rs +++ b/crates/secret-service-client/src/musig2.rs @@ -1,7 +1,7 @@ //! Musig2 signer client use std::{future::Future, sync::Arc}; -use bitcoin::{hashes::Hash, Txid}; +use bitcoin::{hashes::Hash, Txid, XOnlyPublicKey}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::PublicKey, @@ -33,7 +33,7 @@ impl Musig2Client { impl Musig2Signer for Musig2Client { fn new_session( &self, - pubkeys: Vec, + pubkeys: Vec, witness: TaprootWitness, input_txid: Txid, input_vout: u32, @@ -62,7 +62,7 @@ impl Musig2Signer for Musig2Client { } } - fn pubkey(&self) -> impl Future::Container> + Send { + fn pubkey(&self) -> impl Future::Container> + Send { async move { let msg = ClientMessage::Musig2Pubkey; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; @@ -70,7 +70,7 @@ impl Musig2Signer for Musig2Client { return Err(ClientError::WrongMessage(res)); }; - PublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res)) + XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res)) } } } @@ -98,7 +98,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { fn holdouts( &self, - ) -> impl Future::Container>> + Send { + ) -> impl Future::Container>> + Send { async move { let msg = ClientMessage::Musig2FirstRoundHoldouts { session_id: self.session_id, @@ -109,8 +109,8 @@ impl Musig2SignerFirstRound for Musig2FirstRound { }; pubkeys .into_iter() - .map(|pk| PublicKey::from_slice(&pk)) - .collect::, musig2::secp256k1::Error>>() + .map(|pk| XOnlyPublicKey::from_slice(&pk)) + .collect::, musig2::secp256k1::Error>>() .map_err(|_| ClientError::BadData) } } @@ -130,7 +130,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { fn receive_pub_nonce( &mut self, - pubkey: PublicKey, + pubkey: XOnlyPublicKey, pubnonce: PubNonce, ) -> impl Future::Container>> + Send { @@ -197,7 +197,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { fn holdouts( &self, - ) -> impl Future::Container>> + Send { + ) -> impl Future::Container>> + Send { async move { let msg = ClientMessage::Musig2SecondRoundHoldouts { session_id: self.session_id, @@ -208,8 +208,8 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; pubkeys .into_iter() - .map(|pk| PublicKey::from_slice(&pk)) - .collect::, musig2::secp256k1::Error>>() + .map(|pk| XOnlyPublicKey::from_slice(&pk)) + .collect::, musig2::secp256k1::Error>>() .map_err(|_| ClientError::BadData) } } @@ -244,7 +244,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { fn receive_signature( &mut self, - pubkey: PublicKey, + pubkey: XOnlyPublicKey, signature: musig2::PartialSignature, ) -> impl Future::Container>> + Send { diff --git a/crates/secret-service-client/src/operator.rs b/crates/secret-service-client/src/operator.rs index 6bfcfffa..15db9242 100644 --- a/crates/secret-service-client/src/operator.rs +++ b/crates/secret-service-client/src/operator.rs @@ -1,6 +1,7 @@ //! Operator signer client use std::{future::Future, sync::Arc}; +use bitcoin::XOnlyPublicKey; use musig2::secp256k1::{schnorr::Signature, PublicKey}; use quinn::Connection; use secret_service_proto::v1::{ @@ -40,13 +41,13 @@ impl OperatorSigner for OperatorClient { } } - fn pubkey(&self) -> impl Future::Container> + Send { + fn pubkey(&self) -> impl Future::Container> + Send { async move { let msg = ClientMessage::OperatorPubkey; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; match res { ServerMessage::OperatorPubkey { pubkey } => { - PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) + XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) } _ => Err(ClientError::WrongMessage(res)), } diff --git a/crates/secret-service-client/src/p2p.rs b/crates/secret-service-client/src/p2p.rs index a7022c8a..8548cd3c 100644 --- a/crates/secret-service-client/src/p2p.rs +++ b/crates/secret-service-client/src/p2p.rs @@ -1,6 +1,7 @@ //! P2P signer client use std::{future::Future, sync::Arc}; +use bitcoin::XOnlyPublicKey; use musig2::secp256k1::{schnorr::Signature, PublicKey}; use quinn::Connection; use secret_service_proto::v1::{ @@ -38,14 +39,14 @@ impl P2PSigner for P2PClient { } } - fn pubkey(&self) -> impl Future::Container> + Send { + fn pubkey(&self) -> impl Future::Container> + Send { async move { let msg = ClientMessage::P2PPubkey; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::P2PPubkey { pubkey } = res else { return Err(ClientError::WrongMessage(res)); }; - PublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) + XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) } } } diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 2ff8134a..411c07d3 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -2,7 +2,7 @@ use std::future::Future; -use bitcoin::Txid; +use bitcoin::{Txid, XOnlyPublicKey}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, secp256k1::{schnorr::Signature, PublicKey}, @@ -63,7 +63,7 @@ pub trait OperatorSigner: Send { /// Signs a digest using the operator's private key. fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; /// Returns the public key of the operator's secret key. - fn pubkey(&self) -> impl Future> + Send; + fn pubkey(&self) -> impl Future> + Send; } /// The P2P signer is used for signing messages between operators on the peer-to-peer network. @@ -73,7 +73,7 @@ pub trait P2PSigner: Send { /// Signs a digest using the operator's private key. fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; /// Returns the public key of the operator's secret key. - fn pubkey(&self) -> impl Future> + Send; + fn pubkey(&self) -> impl Future> + Send; } /// Uniquely identifies an in-memory musig2 session on the signing server. @@ -98,13 +98,13 @@ pub trait Musig2Signer: Send + Sync { /// determistically (after addition of our own pubkey if required) before session creation fn new_session( &self, - pubkeys: Vec, + pubkeys: Vec, witness: TaprootWitness, input_txid: Txid, input_vout: u32, ) -> impl Future>> + Send; /// Retrieve the public key associated with this musig2 signer. - fn pubkey(&self) -> impl Future> + Send; + fn pubkey(&self) -> impl Future> + Send; } /// Represents a state-machine-like API for performing musig2 signing. This first round is returned @@ -119,7 +119,7 @@ pub trait Musig2SignerFirstRound: Send + Sync { /// Returns a vector of all signer public keys who we have yet to receive a [`PubNonce`] from. /// Note that this will never return our own public key. - fn holdouts(&self) -> impl Future>> + Send; + fn holdouts(&self) -> impl Future>> + Send; /// Returns true once all public nonces have been received from every signer. fn is_complete(&self) -> impl Future> + Send; @@ -129,7 +129,7 @@ pub trait Musig2SignerFirstRound: Send + Sync { /// different nonce on-file for that signer. fn receive_pub_nonce( &mut self, - pubkey: PublicKey, + pubkey: XOnlyPublicKey, pubnonce: PubNonce, ) -> impl Future>> + Send; @@ -165,7 +165,7 @@ pub trait Musig2SignerSecondRound: Send + Sync { /// Returns a vector of signer public keys from whom we have yet to receive a /// [`PartialSignature`]. Note that since our signature was constructed at the end of the /// first round, this vector will never contain our own public key. - fn holdouts(&self) -> impl Future>> + Send; + fn holdouts(&self) -> impl Future>> + Send; /// Returns the partial signature created during finalization of the first round. fn our_signature(&self) -> impl Future> + Send; @@ -179,7 +179,7 @@ pub trait Musig2SignerSecondRound: Send + Sync { /// signer. fn receive_signature( &mut self, - pubkey: PublicKey, + pubkey: XOnlyPublicKey, signature: PartialSignature, ) -> impl Future>> + Send; diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index e25d1570..bca0ddf7 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -27,7 +27,7 @@ pub enum ServerMessage { /// Response for OperatorSigner::pubkey OperatorPubkey { /// Serialized Schnorr compressed public key for operator signatures - pubkey: [u8; 33], + pubkey: [u8; 32], }, /// Response for P2PSigner::sign @@ -38,7 +38,7 @@ pub enum ServerMessage { /// Response for P2PSigner::pubkey P2PPubkey { /// Serialized Schnorr compressed public key for P2P signatures - pubkey: [u8; 33], + pubkey: [u8; 32], }, /// Response for Musig2Signer::new_session @@ -46,7 +46,7 @@ pub enum ServerMessage { /// Response for Musig2Signer::pubkey Musig2Pubkey { /// Serialized Schnorr compressed public key for Musig2 signatures - pubkey: [u8; 33], + pubkey: [u8; 32], }, /// Response for Musig2SignerFirstRound::our_nonce @@ -58,7 +58,7 @@ pub enum ServerMessage { Musig2FirstRoundHoldouts { /// Serialized Schnorr compressed public keys of signers whose pub nonces /// we do not have - pubkeys: Vec<[u8; 33]>, + pubkeys: Vec<[u8; 32]>, }, /// Response for Musig2SignerFirstRound::is_complete Musig2FirstRoundIsComplete { @@ -84,7 +84,7 @@ pub enum ServerMessage { Musig2SecondRoundHoldouts { /// Serialized Schnorr compressed public keys of signers whose partial signatures /// we do not have for this signing session - pubkeys: Vec<[u8; 33]>, + pubkeys: Vec<[u8; 32]>, }, /// Response for Musig2SignerSecondRound::our_signature Musig2SecondRoundOurSignature { @@ -174,7 +174,7 @@ pub enum ClientMessage { Musig2NewSession { /// Public keys for the signing session. May or may not include our own /// public key. If not present, it should be added. May or may not be sorted. - pubkeys: Vec<[u8; 33]>, + pubkeys: Vec<[u8; 32]>, /// The taproot witness of the input witness: SerializableTaprootWitness, /// Serialized txid of the input tx @@ -205,7 +205,7 @@ pub enum ClientMessage { /// Session that we're requesting for session_id: usize, /// The serialized compressed schnorr pubkey of the signer whose pubnonce this is - pubkey: [u8; 33], + pubkey: [u8; 32], /// Serialized public nonce pubnonce: [u8; 66], }, @@ -242,7 +242,7 @@ pub enum ClientMessage { /// Session that we're requesting for session_id: usize, /// The serialized compressed schnorr pubkey of the signer whose pubnonce this is - pubkey: [u8; 33], + pubkey: [u8; 32], /// That signer's musig2 partial sig signature: [u8; 32], }, diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs index a2fd3f44..44611333 100644 --- a/crates/secret-service-proto/src/wire.rs +++ b/crates/secret-service-proto/src/wire.rs @@ -3,6 +3,7 @@ use rkyv::{ api::high::{to_bytes_in, HighSerializer}, rancor, ser::allocator::ArenaHandle, + to_bytes, util::AlignedVec, Archive, Deserialize, Serialize, }; @@ -17,7 +18,7 @@ trait WireMessageMarker: /// A trait for serializing wire messages pub trait WireMessage { /// Serialize the wire message into an aligned vector using rkyv. - fn serialize(&self) -> Result; + fn serialize(&self) -> Result<([u8; 2], AlignedVec), rancor::Error>; } /// The length unit used for wire messages. @@ -44,19 +45,7 @@ pub enum VersionedServerMessage { impl WireMessageMarker for VersionedServerMessage {} impl WireMessage for T { - fn serialize(&self) -> Result { - let mut aligned_buf = AlignedVec::new(); - aligned_buf.extend_from_slice(&LengthUint::MAX.to_le_bytes()); - let mut aligned_buf = to_bytes_in(self, aligned_buf)?; - let len = aligned_buf.len() - size_of::(); - assert!(len <= LengthUint::MAX as usize); - (len as LengthUint) - .to_le_bytes() - .into_iter() - .enumerate() - .for_each(|byte| { - aligned_buf[byte.0] = byte.1; - }); - Ok(aligned_buf) + fn serialize(&self) -> Result<([u8; 2], AlignedVec), rancor::Error> { + to_bytes(self).map(|b| ((b.len() as LengthUint).to_le_bytes(), b)) } } diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 34913a48..c77a9302 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -7,17 +7,20 @@ pub mod musig2_session_mgr; use std::{io, marker::Sync, net::SocketAddr, sync::Arc}; -use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid}; +use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid, XOnlyPublicKey}; use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; use musig2_session_mgr::Musig2SessionManager; pub use quinn::rustls; use quinn::{ crypto::rustls::{NoInitialCipherSuite, QuicServerConfig}, ConnectionError, Endpoint, Incoming, ReadExactError, RecvStream, SendStream, ServerConfig, + WriteError, }; use rkyv::{ deserialize, rancor::{self, Error}, + to_bytes, + util::AlignedVec, }; use secret_service_proto::{ v1::{ @@ -139,14 +142,19 @@ async fn request_manager( match handler_res { Ok(msg) => { - let byte_response = match WireMessage::serialize(&VersionedServerMessage::V1(msg)) { + let (len_bytes, msg_bytes) = match VersionedServerMessage::V1(msg).serialize() { Ok(r) => r, Err(e) => { error!("failed to serialize response: {e:?}"); return; } }; - if let Err(e) = tx.write_all(&byte_response).await { + let write = || async move { + tx.write_all(&len_bytes).await?; + tx.write_all(&msg_bytes).await?; + Ok::<_, WriteError>(()) + }; + if let Err(e) = write().await { warn!("failed to send response: {e:?}"); } } @@ -170,7 +178,8 @@ where LengthUint::from_le_bytes(buf) }; - let mut buf = vec![0u8; len_to_read as usize]; + let mut buf = AlignedVec::<16>::with_capacity(len_to_read as usize); + buf.resize(len_to_read as usize, 0); rx.read_exact(&mut buf).await?; let msg = rkyv::access::(&buf).unwrap(); @@ -222,7 +231,7 @@ where }; let Ok(pubkeys) = pubkeys .into_iter() - .map(|data| PublicKey::from_slice(data)) + .map(|data| XOnlyPublicKey::from_slice(data)) .collect::, _>>() else { break 'block ServerMessage::InvalidClientMessage; @@ -278,7 +287,7 @@ where .holdouts() .await .iter() - .map(PublicKey::serialize) + .map(XOnlyPublicKey::serialize) .collect(), }, _ => ServerMessage::InvalidClientMessage, @@ -303,7 +312,7 @@ where } => { let session_id = session_id.to_native() as usize; let r = musig2_sm.lock().await.first_round(session_id); - let pubkey = PublicKey::from_slice(pubkey); + let pubkey = XOnlyPublicKey::from_slice(pubkey); let pubnonce = PubNonce::from_bytes(pubnonce); match (r, pubkey, pubnonce) { (Ok(Some(first_round)), Ok(pubkey), Ok(pubnonce)) => { @@ -363,7 +372,7 @@ where .holdouts() .await .iter() - .map(PublicKey::serialize) + .map(XOnlyPublicKey::serialize) .collect(), }, _ => ServerMessage::InvalidClientMessage, @@ -402,7 +411,7 @@ where } => { let session_id = session_id.to_native() as usize; let sr = musig2_sm.lock().await.second_round(session_id); - let pubkey = PublicKey::from_slice(pubkey); + let pubkey = XOnlyPublicKey::from_slice(pubkey); let signature = PartialSignature::from_slice(signature); match (sr, pubkey, signature) { (Ok(Some(sr)), Ok(pubkey), Ok(signature)) => { diff --git a/crates/secret-service/Cargo.toml b/crates/secret-service/Cargo.toml index 1b40191a..6d389045 100644 --- a/crates/secret-service/Cargo.toml +++ b/crates/secret-service/Cargo.toml @@ -24,6 +24,10 @@ toml = "0.8.19" tracing.workspace = true tracing-subscriber.workspace = true +[dev-dependencies] +rcgen = "0.13.2" +secret-service-client = { path = "../secret-service-client" } + [lints] rust.missing_debug_implementations = "warn" rust.rust_2018_idioms = { level = "deny", priority = -1 } diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 7a850bb5..56fe3d14 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -53,14 +53,17 @@ impl Service { Err(e) => return Err(e), }; + Ok(Self::new_with_seed(seed)) + } + + pub fn new_with_seed(seed: [u8; 32]) -> Self { let keys = OperatorKeys::new(&Xpriv::new_master(NETWORK, &seed).expect("valid xpriv")) .expect("valid keychain"); - info!( "Master fingerprint: {}", keys.master_xpub().fingerprint().to_string().bold() ); - Ok(Self { keys }) + Self { keys } } } diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index f38e5230..ea264a89 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -3,8 +3,8 @@ use std::future::Future; use bitcoin::{ bip32::{ChildNumber, Xpriv}, hashes::Hash, - key::Keypair, - Txid, + key::{Keypair, Parity}, + Txid, XOnlyPublicKey, }; use hkdf::Hkdf; use make_buf::make_buf; @@ -58,21 +58,20 @@ impl Ms2Signer { impl Musig2Signer for Ms2Signer { fn new_session( &self, - mut pubkeys: Vec, + mut pubkeys: Vec, witness: TaprootWitness, input_txid: Txid, input_vout: u32, ) -> impl Future> + Send { async move { - if !pubkeys.contains(&self.kp.public_key()) { - pubkeys.push(self.kp.public_key()); + let my_pub_key = self.kp.x_only_public_key().0; + if !pubkeys.contains(&my_pub_key) { + pubkeys.push(my_pub_key); } pubkeys.sort(); - let signer_index = pubkeys - .iter() - .position(|pk| pk == &self.kp.public_key()) - .unwrap(); - let mut ctx = KeyAggContext::new(pubkeys.clone()).unwrap(); + let signer_index = pubkeys.iter().position(|pk| pk == &my_pub_key).unwrap(); + let mut ctx = + KeyAggContext::new(pubkeys.iter().map(|pk| pk.public_key(Parity::Even))).unwrap(); match witness { TaprootWitness::Key => { @@ -118,14 +117,14 @@ impl Musig2Signer for Ms2Signer { } } - fn pubkey(&self) -> impl Future::Container> + Send { - async move { self.kp.public_key() } + fn pubkey(&self) -> impl Future::Container> + Send { + async move { self.kp.x_only_public_key().0 } } } pub struct ServerFirstRound { first_round: FirstRound, - ordered_public_keys: Vec, + ordered_public_keys: Vec, seckey: SecretKey, } @@ -138,7 +137,7 @@ impl Musig2SignerFirstRound for ServerFirstRound { fn holdouts( &self, - ) -> impl Future::Container>> + Send { + ) -> impl Future::Container>> + Send { async move { self.first_round .holdouts() @@ -154,7 +153,7 @@ impl Musig2SignerFirstRound for ServerFirstRound { fn receive_pub_nonce( &mut self, - pubkey: PublicKey, + pubkey: XOnlyPublicKey, pubnonce: musig2::PubNonce, ) -> impl Future::Container>> + Send { @@ -187,7 +186,7 @@ impl Musig2SignerFirstRound for ServerFirstRound { pub struct ServerSecondRound { second_round: SecondRound<[u8; 32]>, - ordered_public_keys: Vec, + ordered_public_keys: Vec, } impl Musig2SignerSecondRound for ServerSecondRound { @@ -199,7 +198,7 @@ impl Musig2SignerSecondRound for ServerSecondRound { fn holdouts( &self, - ) -> impl Future::Container>> + Send { + ) -> impl Future::Container>> + Send { async move { self.second_round .holdouts() @@ -221,7 +220,7 @@ impl Musig2SignerSecondRound for ServerSecondRound { fn receive_signature( &mut self, - pubkey: PublicKey, + pubkey: XOnlyPublicKey, signature: musig2::PartialSignature, ) -> impl Future::Container>> + Send { diff --git a/crates/secret-service/src/disk/operator.rs b/crates/secret-service/src/disk/operator.rs index 25c3a52c..be74fb9b 100644 --- a/crates/secret-service/src/disk/operator.rs +++ b/crates/secret-service/src/disk/operator.rs @@ -1,6 +1,6 @@ use std::future::Future; -use bitcoin::key::Keypair; +use bitcoin::{key::Keypair, XOnlyPublicKey}; use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{OperatorSigner, Origin, Server}; @@ -26,7 +26,7 @@ impl OperatorSigner for Operator { } } - fn pubkey(&self) -> impl Future::Container> + Send { - async move { self.kp.public_key() } + fn pubkey(&self) -> impl Future::Container> + Send { + async move { self.kp.x_only_public_key().0 } } } diff --git a/crates/secret-service/src/disk/p2p.rs b/crates/secret-service/src/disk/p2p.rs index 3933ac96..3f040f36 100644 --- a/crates/secret-service/src/disk/p2p.rs +++ b/crates/secret-service/src/disk/p2p.rs @@ -1,6 +1,6 @@ use std::future::Future; -use bitcoin::key::Keypair; +use bitcoin::{key::Keypair, XOnlyPublicKey}; use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{P2PSigner, Server}; @@ -17,13 +17,10 @@ impl ServerP2PSigner { impl P2PSigner for ServerP2PSigner { fn sign(&self, digest: &[u8; 32]) -> impl Future + Send { - async move { - self.kp - .sign_schnorr(Message::from_digest_slice(digest).unwrap()) - } + async move { self.kp.sign_schnorr(Message::from_digest(*digest)) } } - fn pubkey(&self) -> impl Future + Send { - async move { self.kp.public_key() } + fn pubkey(&self) -> impl Future + Send { + async move { self.kp.x_only_public_key().0 } } } diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 66887669..df5b17a9 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -2,6 +2,8 @@ pub mod config; pub mod disk; +#[cfg(test)] +mod tests; mod tls; use std::{env::args, path::PathBuf, str::FromStr, sync::LazyLock}; diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs new file mode 100644 index 00000000..80a1bdb7 --- /dev/null +++ b/crates/secret-service/src/tests.rs @@ -0,0 +1,138 @@ +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + sync::Arc, + time::Duration, +}; + +use bitcoin::{key::Secp256k1, XOnlyPublicKey}; +use musig2::secp256k1::Message; +use rand::{thread_rng, Rng}; +use secret_service_client::SecretServiceClient; +use secret_service_proto::v1::traits::{OperatorSigner, P2PSigner, SecretService}; +use secret_service_server::{ + run_server, + rustls::{ + self, + pki_types::{CertificateDer, PrivatePkcs8KeyDer, ServerName, UnixTime}, + ClientConfig, ServerConfig, + }, +}; + +use crate::disk::Service; + +#[tokio::test] +async fn e2e() { + let server_addr: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 20000).into(); + let server_host = "localhost".to_string(); + + let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); + let key = PrivatePkcs8KeyDer::from(cert.key_pair.serialize_der()); + let cert = cert.cert.into(); + let server_tls_config = ServerConfig::builder() + .with_no_client_auth() + .with_single_cert(vec![cert], key.into()) + .expect("valid config"); + let config = secret_service_server::Config { + addr: server_addr.clone(), + tls_config: server_tls_config, + connection_limit: None, + }; + let service = Service::new_with_seed([0u8; 32]); + + tokio::spawn(async move { + run_server(config, service.into()).await.unwrap(); + }); + + let client_tls = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new()) + .with_no_client_auth(); + let client_config = secret_service_client::Config { + server_addr, + server_hostname: server_host, + local_addr: None, + tls_config: client_tls, + timeout: Duration::from_secs(1), + }; + + let client = SecretServiceClient::new(client_config) + .await + .expect("good conn"); + + let mut rng = thread_rng(); + let secp_ctx = Secp256k1::verification_only(); + + // operator signer + let op_signer = client.operator_signer(); + let pubkey = op_signer.pubkey().await.expect("good response"); + let to_sign = rng.gen(); + let sig = op_signer.sign(&to_sign).await.expect("good response"); + assert!(secp_ctx + .verify_schnorr(&sig, &Message::from_digest(to_sign), &pubkey) + .is_ok()); + + // p2p signer + let p2p_signer = client.p2p_signer(); + let pubkey = p2p_signer.pubkey().await.expect("good response"); + let to_sign = rng.gen(); + let sig = p2p_signer.sign(&to_sign).await.expect("good response"); + assert!(secp_ctx + .verify_schnorr(&sig, &Message::from_digest(to_sign), &pubkey) + .is_ok()); +} + +/// Dummy certificate verifier that treats any certificate as valid. +/// NOTE, such verification is vulnerable to MITM attacks, but convenient for testing. +#[derive(Debug)] +struct SkipServerVerification(Arc); + +impl SkipServerVerification { + fn new() -> Arc { + Arc::new(Self(Arc::new(rustls::crypto::ring::default_provider()))) + } +} + +impl rustls::client::danger::ServerCertVerifier for SkipServerVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls12_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &rustls::DigitallySignedStruct, + ) -> Result { + rustls::crypto::verify_tls13_signature( + message, + cert, + dss, + &self.0.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + self.0.signature_verification_algorithms.supported_schemes() + } +} From 25f8d6bff052e1e8d11a4f212001ded5dff98f74 Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 24 Feb 2025 11:57:56 +0000 Subject: [PATCH 21/30] attempt to fix rebased cargo.toml --- Cargo.toml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 108a875c..a7059eb4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,9 +66,13 @@ zkaleido-sp1-adapter = { git = "https://github.com/alpenlabs/zkaleido", tag = "v anyhow = "1.0.95" arbitrary = { version = "1.4.1", features = ["derive"] } ark-bn254 = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } +ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives/" } ark-ec = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } ark-ff = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } ark-groth16 = { git = "https://github.com/arkworks-rs/groth16" } +ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std/" } +ark-relations = { git = "https://github.com/arkworks-rs/snark/" } +ark-std = { git = "https://github.com/arkworks-rs/std/" } async-trait = "0.1.81" base64 = "0.22.1" bincode = "1.3.3" @@ -76,18 +80,20 @@ bitcoin = { version = "0.32.5", features = ["rand-std", "serde"] } bitcoin-bosd = "0.4.0" bitcoin-script = { git = "https://github.com/BitVM/rust-bitcoin-script" } bitvm = { git = "https://github.com/alpenlabs/BitVM.git", branch = "testnet-i" } -blake3 = { version = "1.5", features = ["zeroize"] } borsh = { version = "1.5.0", features = ["derive"] } chrono = "0.4.38" clap = { version = "4.5.20", features = ["cargo", "derive", "env"] } corepc-node = { version = "0.5.0", features = ["28_0", "download"] } +dotenvy = "0.15.7" esplora-client = { git = "https://github.com/BitVM/rust-esplora-client", default-features = false, features = [ "blocking-https-rustls", "async-https-rustls", ] } +ethnum = "1.5.0" futures = "0.3.31" hex = { version = "0.4", features = ["serde"] } jsonrpsee = "0.24.7" +jsonrpsee-types = "0.24.7" kanal = "0.1.0-pre8" musig2 = { version = "0.1.0", features = [ "serde", @@ -110,6 +116,7 @@ serde_json = { version = "1.0", default-features = false, features = [ "alloc", "raw_value", ] } +serde_with = "3.12.0" sha2 = "0.10" sqlx = { version = "0.8.2", features = [ "sqlite", @@ -119,6 +126,7 @@ sqlx = { version = "0.8.2", features = [ "derive", "migrate", ] } +tempfile = "3.10.1" terrors = "0.3.2" thiserror = "2.0.3" tokio = { version = "1.37", features = ["full"] } From 8f3f00a25f7ce3129539b252e2eeddc331296753 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Mon, 24 Feb 2025 09:47:48 -0300 Subject: [PATCH 22/30] Additional docs for the Secret Service (#44) * doc: secret-service-client * doc: secret-service-proto wire * doc: secret-service-proto traits * doc: secret-service-proto rhyv_wrappers * doc: secret-service-server * doc: secret-service * doc: S2 is too ambiguous... * Apply suggestions from code review Co-authored-by: azz * doc: rebase * doc: add spaces --------- Co-authored-by: azz --- crates/secret-service-client/src/lib.rs | 41 ++- crates/secret-service-client/src/musig2.rs | 24 +- crates/secret-service-client/src/operator.rs | 7 + crates/secret-service-client/src/p2p.rs | 7 + .../secret-service-client/src/stakechain.rs | 11 +- crates/secret-service-client/src/wots.rs | 17 +- crates/secret-service-proto/src/lib.rs | 2 + crates/secret-service-proto/src/v1/mod.rs | 2 +- .../src/v1/rkyv_wrappers.rs | 23 ++ crates/secret-service-proto/src/v1/traits.rs | 196 +++++++----- crates/secret-service-proto/src/v1/wire.rs | 287 ++++++++++++------ crates/secret-service-proto/src/wire.rs | 5 +- crates/secret-service-server/src/bool_arr.rs | 21 +- crates/secret-service-server/src/lib.rs | 22 +- .../src/musig2_session_mgr.rs | 71 +++-- crates/secret-service/src/config.rs | 22 +- crates/secret-service/src/disk/mod.rs | 6 + crates/secret-service/src/disk/musig2.rs | 20 ++ crates/secret-service/src/disk/operator.rs | 6 + crates/secret-service/src/disk/p2p.rs | 6 + crates/secret-service/src/disk/stakechain.rs | 9 + crates/secret-service/src/disk/wots.rs | 12 +- crates/secret-service/src/main.rs | 6 +- crates/secret-service/src/tls.rs | 4 + 24 files changed, 572 insertions(+), 255 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index d950ebfc..d133b8bf 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -1,6 +1,7 @@ //! The client crate for the secret service. Provides implementations of the traits that use a QUIC //! connection and wire protocol defined in the [`secret_service_proto`] crate to connect with a //! remote secret service. + pub mod musig2; pub mod operator; pub mod p2p; @@ -38,31 +39,41 @@ use terrors::OneOf; use tokio::time::timeout; use wots::WotsClient; -/// Configuration for the S2 client +/// Configuration for the Secret Service client. #[derive(Clone, Debug)] pub struct Config { - /// Server to connect to - pub server_addr: SocketAddr, - /// Hostname present on the server's certificate - pub server_hostname: String, - /// Optional local socket to connect via - pub local_addr: Option, - /// Config for TLS. Note that you should be verifying the server's identity via this to prevent - /// MITM attacks. - pub tls_config: rustls::ClientConfig, - /// Timeout for requests - pub timeout: Duration, + /// Server to connect to. + server_addr: SocketAddr, + + /// Hostname present on the server's certificate. + server_hostname: String, + + /// Optional local socket to connect via. + local_addr: Option, + + /// Config for TLS. + /// + /// # Warning + /// + /// Users should always be verifying the server's identity via this to prevent MITM attacks. + tls_config: rustls::ClientConfig, + + /// Timeout for requests. + timeout: Duration, } -/// A client that connects to a remote secret service via QUIC +/// A client that connects to a remote secret service via QUIC. #[derive(Clone, Debug)] pub struct SecretServiceClient { + /// Client configuration. config: Arc, + + /// QUIC connection to the server. conn: Connection, } impl SecretServiceClient { - /// Create a new client and attempt to connect to the server. + /// Creates a new client and attempt to connect to the server. pub async fn new( config: Config, ) -> Result< @@ -131,7 +142,7 @@ impl SecretService for SecretServic } } -/// Makes a v1 secret service request via quic +/// Makes a v1 secret service request via QUIC. pub async fn make_v1_req( conn: &Connection, msg: ClientMessage, diff --git a/crates/secret-service-client/src/musig2.rs b/crates/secret-service-client/src/musig2.rs index 95e1115f..fecf73fe 100644 --- a/crates/secret-service-client/src/musig2.rs +++ b/crates/secret-service-client/src/musig2.rs @@ -1,4 +1,5 @@ -//! Musig2 signer client +//! MuSig2 signer client + use std::{future::Future, sync::Arc}; use bitcoin::{hashes::Hash, Txid, XOnlyPublicKey}; @@ -19,12 +20,18 @@ use strata_bridge_primitives::scripts::taproot::TaprootWitness; use crate::{make_v1_req, Config}; +/// MuSig2 client. +#[derive(Debug, Clone)] pub struct Musig2Client { + /// QUIC connection to the server. conn: Connection, + + /// Configuration for the client. config: Arc, } impl Musig2Client { + /// Creates a new MuSig2 client with an existing QUIC connection and configuration. pub fn new(conn: Connection, config: Arc) -> Self { Self { conn, config } } @@ -75,10 +82,16 @@ impl Musig2Signer for Musig2Client { } } -#[derive(Clone)] +/// The first round of the MuSig2 protocol. +#[derive(Debug, Clone)] pub struct Musig2FirstRound { + /// The MuSig2 session ID. session_id: Musig2SessionId, + + /// The connection to the server. connection: Connection, + + /// The configuration for the client. config: Arc, } @@ -175,9 +188,16 @@ impl Musig2SignerFirstRound for Musig2FirstRound { } } +/// The second round of the MuSig2 protocol. +#[derive(Debug, Clone)] pub struct Musig2SecondRound { + /// The MuSig2 session ID. session_id: Musig2SessionId, + + /// The connection to the server. connection: Connection, + + /// The configuration for the client. config: Arc, } diff --git a/crates/secret-service-client/src/operator.rs b/crates/secret-service-client/src/operator.rs index 15db9242..6d07bfc5 100644 --- a/crates/secret-service-client/src/operator.rs +++ b/crates/secret-service-client/src/operator.rs @@ -1,4 +1,5 @@ //! Operator signer client + use std::{future::Future, sync::Arc}; use bitcoin::XOnlyPublicKey; @@ -11,12 +12,18 @@ use secret_service_proto::v1::{ use crate::{make_v1_req, Config}; +/// Operator signer client. +#[derive(Debug, Clone)] pub struct OperatorClient { + /// QUIC connection to the server. conn: Connection, + + /// Configuration for the client. config: Arc, } impl OperatorClient { + /// Creates a new operator client with an existing QUIC connection and configuration. pub fn new(conn: Connection, config: Arc) -> Self { Self { conn, config } } diff --git a/crates/secret-service-client/src/p2p.rs b/crates/secret-service-client/src/p2p.rs index 8548cd3c..ddad6e4d 100644 --- a/crates/secret-service-client/src/p2p.rs +++ b/crates/secret-service-client/src/p2p.rs @@ -1,4 +1,5 @@ //! P2P signer client + use std::{future::Future, sync::Arc}; use bitcoin::XOnlyPublicKey; @@ -11,12 +12,18 @@ use secret_service_proto::v1::{ use crate::{make_v1_req, Config}; +/// P2P signer client. +#[derive(Debug, Clone)] pub struct P2PClient { + /// QUIC connection to the server. conn: Connection, + + /// Configuration for the client. config: Arc, } impl P2PClient { + /// Creates a new P2P client with an existing QUIC connection and configuration. pub fn new(conn: Connection, config: Arc) -> Self { Self { conn, config } } diff --git a/crates/secret-service-client/src/stakechain.rs b/crates/secret-service-client/src/stakechain.rs index 5832b50d..e679ea81 100644 --- a/crates/secret-service-client/src/stakechain.rs +++ b/crates/secret-service-client/src/stakechain.rs @@ -1,4 +1,5 @@ -//! Stakechain preimages client +//! Stake Chain preimages client + use std::{future::Future, sync::Arc}; use bitcoin::{hashes::Hash, Txid}; @@ -10,13 +11,19 @@ use secret_service_proto::v1::{ use crate::{make_v1_req, Config}; +/// Stake Chain preimages client. +#[derive(Debug, Clone)] pub struct StakeChainPreimgClient { + /// QUIC connection to the server. conn: Connection, + + /// Configuration for the client. config: Arc, } impl StakeChainPreimgClient { - /// Guess? + /// Creates a new Stake Chain preimages client with an existing QUIC connection and + /// configuration. pub fn new(conn: Connection, config: Arc) -> Self { Self { conn, config } } diff --git a/crates/secret-service-client/src/wots.rs b/crates/secret-service-client/src/wots.rs index 589c500a..a678863a 100644 --- a/crates/secret-service-client/src/wots.rs +++ b/crates/secret-service-client/src/wots.rs @@ -1,4 +1,4 @@ -//! WOTS signer client +//! Winternitz One-time Signature (WOTS) signer client use std::{future::Future, sync::Arc}; use bitcoin::{hashes::Hash, Txid}; @@ -10,13 +10,18 @@ use secret_service_proto::v1::{ use crate::{make_v1_req, Config}; +/// Winternitz One-time Signature (WOTS) signer client. +#[derive(Debug, Clone)] pub struct WotsClient { + /// QUIC connection to the server. conn: Connection, + + /// Configuration for the client. config: Arc, } impl WotsClient { - /// Creates a new wots client with an existing quic connection and config + /// Creates a new WOTS client with an existing QUIC connection and configuration. pub fn new(conn: Connection, config: Arc) -> Self { Self { conn, config } } @@ -25,9 +30,9 @@ impl WotsClient { impl WotsSigner for WotsClient { fn get_160_key( &self, - index: u32, - vout: u32, txid: Txid, + vout: u32, + index: u32, ) -> impl Future::Container<[u8; 20 * 160]>> + Send { async move { let msg = ClientMessage::WotsGet160Key { @@ -45,9 +50,9 @@ impl WotsSigner for WotsClient { fn get_256_key( &self, - index: u32, - vout: u32, txid: Txid, + vout: u32, + index: u32, ) -> impl Future::Container<[u8; 20 * 256]>> + Send { async move { let msg = ClientMessage::WotsGet256Key { diff --git a/crates/secret-service-proto/src/lib.rs b/crates/secret-service-proto/src/lib.rs index 8f2d6bbc..a9a89841 100644 --- a/crates/secret-service-proto/src/lib.rs +++ b/crates/secret-service-proto/src/lib.rs @@ -1,2 +1,4 @@ +//! Protocol definitions for the Secret Service. + pub mod v1; pub mod wire; diff --git a/crates/secret-service-proto/src/v1/mod.rs b/crates/secret-service-proto/src/v1/mod.rs index 27374c0c..2b5fa1da 100644 --- a/crates/secret-service-proto/src/v1/mod.rs +++ b/crates/secret-service-proto/src/v1/mod.rs @@ -1,5 +1,5 @@ //! V1 secret service -#[allow(missing_docs)] + pub mod rkyv_wrappers; pub mod traits; pub mod wire; diff --git a/crates/secret-service-proto/src/v1/rkyv_wrappers.rs b/crates/secret-service-proto/src/v1/rkyv_wrappers.rs index 446935e0..dc3eefdd 100644 --- a/crates/secret-service-proto/src/v1/rkyv_wrappers.rs +++ b/crates/secret-service-proto/src/v1/rkyv_wrappers.rs @@ -7,12 +7,15 @@ use rkyv::{Archive, Deserialize, Serialize}; +/// Wrapper for [`musig2::errors::RoundContributionError`]. #[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] #[rkyv(remote = musig2::errors::RoundContributionError)] #[rkyv(archived = ArchivedRoundContributionError)] pub struct RoundContributionError { + /// The index of the contributor. pub index: usize, + /// The reason for the error. #[rkyv(with = ContributionFaultReason)] pub reason: musig2::errors::ContributionFaultReason, } @@ -35,12 +38,18 @@ impl From for musig2::errors::RoundContributionError { } } +/// Wrapper for [`musig2::errors::ContributionFaultReason`]. #[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] #[rkyv(remote = musig2::errors::ContributionFaultReason)] #[rkyv(archived = ArchivedContributionFaultReason)] pub enum ContributionFaultReason { + /// The index is out of range. OutOfRange(usize), + + /// The contribution is inconsistent. InconsistentContribution, + + /// The signature is invalid. InvalidSignature, } @@ -66,12 +75,18 @@ impl From for musig2::errors::ContributionFaultReason { } } +/// Wrapper for [`musig2::errors::RoundFinalizeError`]. #[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] #[rkyv(remote = musig2::errors::RoundFinalizeError)] #[rkyv(archived = ArchivedRoundFinalizeError)] pub enum RoundFinalizeError { + /// The round was not completed. Incomplete, + + /// Wrapper for [`musig2::errors::SigningError`]. SigningError(#[rkyv(with = SigningError)] musig2::errors::SigningError), + + /// Wrapper for [`musig2::errors::VerifyError`]. InvalidAggregatedSignature(#[rkyv(with = VerifyError)] musig2::errors::VerifyError), } @@ -101,11 +116,15 @@ impl From for musig2::errors::RoundFinalizeError { } } +/// Wrapper for [`musig2::errors::SigningError`]. #[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] #[rkyv(remote = musig2::errors::SigningError)] #[rkyv(archived = ArchivedSigningError)] pub enum SigningError { + /// Unknown key. UnknownKey, + + /// Self verification failed. SelfVerifyFail, } @@ -127,11 +146,15 @@ impl From for musig2::errors::SigningError { } } +/// Wrapper for [`musig2::errors::VerifyError`]. #[derive(Debug, PartialEq, Eq, Clone, Archive, Serialize, Deserialize)] #[rkyv(remote = musig2::errors::VerifyError)] #[rkyv(archived = ArchivedVerifyError)] pub enum VerifyError { + /// Unknown key. UnknownKey, + + /// Bad signature. BadSignature, } diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 411c07d3..a8b81d8e 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -5,7 +5,7 @@ use std::future::Future; use bitcoin::{Txid, XOnlyPublicKey}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::{schnorr::Signature, PublicKey}, + secp256k1::schnorr::Signature, AggNonce, LiftedSignature, PartialSignature, PubNonce, }; use quinn::{ConnectionError, ReadExactError, WriteError}; @@ -14,88 +14,104 @@ use strata_bridge_primitives::scripts::taproot::TaprootWitness; use super::wire::ServerMessage; -// possible when https://github.com/rust-lang/rust/issues/63063 is stabliized +// FIXME: possible when https://github.com/rust-lang/rust/issues/63063 is stabliized // pub type AsyncResult = impl Future>; -/// The SecretService trait is the core interface for the Secret Service, -/// implemented by both the client and the server with different versions. +/// Core interface for the Secret Service, implemented by both the client and the server with +/// different versions. pub trait SecretService: Send where O: Origin, FirstRound: Musig2SignerFirstRound, { - /// Implementation of the OperatorSigner trait. + /// Implementation of the [`OperatorSigner`] trait. type OperatorSigner: OperatorSigner; - /// Implementation of the P2PSigner trait. + /// Implementation of the [`P2PSigner`] trait. type P2PSigner: P2PSigner; - /// Implementation of the Musig2Signer trait. + /// Implementation of the [`Musig2Signer`] trait. type Musig2Signer: Musig2Signer; - /// Implementation of the WotsSigner trait. + /// Implementation of the [`WotsSigner`] trait. type WotsSigner: WotsSigner; - /// Implementation of the StakeChainPreimages trait. + /// Implementation of the [`StakeChainPreimages`] trait. type StakeChainPreimages: StakeChainPreimages; - /// Creates an instance of the OperatorSigner. + /// Creates an instance of the [`OperatorSigner`]. fn operator_signer(&self) -> Self::OperatorSigner; - /// Creates an instance of the P2PSigner. + /// Creates an instance of the [`P2PSigner`]. fn p2p_signer(&self) -> Self::P2PSigner; - /// Creates an instance of the Musig2Signer. + /// Creates an instance of the [`Musig2Signer`]. fn musig2_signer(&self) -> Self::Musig2Signer; - /// Creates an instance of the WotsSigner. + /// Creates an instance of the [`WotsSigner`]. fn wots_signer(&self) -> Self::WotsSigner; - /// Creates an instance of the StakeChainPreimages. + /// Creates an instance of the [`StakeChainPreimages`]. fn stake_chain_preimages(&self) -> Self::StakeChainPreimages; } /// The operator signer signs transactions for the operator's own wallet that /// is used for fronting withdrawals and other operations. /// -/// This should have its own unique key that isn't used for any other purpose. +/// # Warning +/// +/// The user should make sure the operator's secret key should have its own unique key that isn't +/// used for any other purpose. pub trait OperatorSigner: Send { - /// Signs a digest using the operator's private key. + /// Signs a `digest` using the operator's [`SecretKey`](bitcoin::secp256k1::SecretKey). fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; + /// Returns the public key of the operator's secret key. fn pubkey(&self) -> impl Future> + Send; } /// The P2P signer is used for signing messages between operators on the peer-to-peer network. /// -/// This should have its own unique key that isn't used for any other purpose. +/// # Warning +/// +/// The user should make sure the operator's secret key should have its own unique key that isn't +/// used for any other purpose. pub trait P2PSigner: Send { - /// Signs a digest using the operator's private key. + /// Signs a `digest` using the operator's [`SecretKey`](bitcoin::secp256k1::SecretKey). fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; + /// Returns the public key of the operator's secret key. fn pubkey(&self) -> impl Future> + Send; } -/// Uniquely identifies an in-memory musig2 session on the signing server. +/// Uniquely identifies an in-memory MuSig2 session on the signing server. pub type Musig2SessionId = usize; /// Error returned when trying to access a signer that is out of bounds. #[derive(Debug, Archive, Serialize, Deserialize, Clone)] pub struct SignerIdxOutOfBounds { - /// Index we were trying to access. + /// Index tried to access. pub index: usize, + /// Number of signers in the session. pub n_signers: usize, } -/// The musig2 signer trait is used to bootstrap and begin a musig2 session. +/// The MuSig2 signer trait is used to bootstrap and initialize a MuSig2 session. +/// +/// # Warning +/// /// A single secret key should be used across all sessions initiated by this signer, -/// whose public key should be accessible via the `pubkey` method. +/// whose public key should be accessible via the [`Musig2Signer::pubkey`] method. pub trait Musig2Signer: Send + Sync { - /// Initialize a new musig2 session with the given public keys, witness, input transaction ID, - /// and input vout. `pubkeys` may or may not include our own pubkey and should be added if not - /// included by implementor. `pubkeys` may or may not be sorted, so should be sorted - /// determistically (after addition of our own pubkey if required) before session creation + /// Initializes a new MuSig2 session with the given public keys, witness, input transaction ID, + /// and input vout. + /// + /// # Warning + /// + /// `pubkeys` may or may not include our own pubkey and should be added if not + /// included by implementer. `pubkeys` may or may not be sorted, so should be sorted + /// deterministically (after addition of our own pubkey if required) before session creation. fn new_session( &self, pubkeys: Vec, @@ -103,29 +119,38 @@ pub trait Musig2Signer: Send + Sync { input_txid: Txid, input_vout: u32, ) -> impl Future>> + Send; - /// Retrieve the public key associated with this musig2 signer. + + /// Retrieves the public key associated with this MuSig2 signer. fn pubkey(&self) -> impl Future> + Send; } -/// Represents a state-machine-like API for performing musig2 signing. This first round is returned -/// by the `new_session` method of the `Musig2Signer` trait. +/// Represents a state-machine-like API for performing MuSig2 signing. /// -/// This enables ergonomic usage of the (relatively) complex musig2 signing process via generics. -/// The secret-service-client crate provides a client-side implementation of this trait, and -/// implementors should provide their own implementation server-side. +/// This first round is returned by the [`Musig2Signer::new_session`] method of the [`Musig2Signer`] +/// trait. +/// +/// # Implementation Details +/// +/// This enables ergonomic usage of the (relatively) complex MuSig2 signing process via generics. +/// The `secret-service-client` crate provides a client-side implementation of this trait, and +/// implementers should provide their own implementation server-side. pub trait Musig2SignerFirstRound: Send + Sync { - /// Returns our public nonce which should be shared with other signers. + /// Returns the client's public nonce which should be shared with other signers. fn our_nonce(&self) -> impl Future> + Send; - /// Returns a vector of all signer public keys who we have yet to receive a [`PubNonce`] from. + /// Returns a vector of all signer public keys who the client have yet to receive a [`PubNonce`] + /// from. + /// /// Note that this will never return our own public key. fn holdouts(&self) -> impl Future>> + Send; - /// Returns true once all public nonces have been received from every signer. + /// Returns `true` once all public nonces have been received from every signer. fn is_complete(&self) -> impl Future> + Send; /// Adds a [`PubNonce`] to the internal state, registering it to a specific signer at a given - /// index. Returns an error if the signer index is out of range, or if we already have a + /// index. + /// + /// Returns an error if the signer index is out of range, or if the client already have a /// different nonce on-file for that signer. fn receive_pub_nonce( &mut self, @@ -152,7 +177,7 @@ pub trait Musig2SignerFirstRound: Send + Sync { ) -> impl Future>> + Send; } -/// This trait represents the second round of the musig2 signing process. +/// This trait represents the second round of the MuSig2 signing process. /// It is responsible for aggregating the partial signatures into a single /// signature, and for verifying the aggregated signature. pub trait Musig2SignerSecondRound: Send + Sync { @@ -162,7 +187,7 @@ pub trait Musig2SignerSecondRound: Send + Sync { /// pair of signers. fn agg_nonce(&self) -> impl Future> + Send; - /// Returns a vector of signer public keys from whom we have yet to receive a + /// Returns a vector of signer public keys from whom the server have yet to receive a /// [`PartialSignature`]. Note that since our signature was constructed at the end of the /// first round, this vector will never contain our own public key. fn holdouts(&self) -> impl Future>> + Send; @@ -170,13 +195,13 @@ pub trait Musig2SignerSecondRound: Send + Sync { /// Returns the partial signature created during finalization of the first round. fn our_signature(&self) -> impl Future> + Send; - /// Returns true once we have all partial signatures from the group. + /// Returns true once the server have all partial signatures from the group. fn is_complete(&self) -> impl Future> + Send; /// Adds a [`PartialSignature`] to the internal state, registering it to a specific signer. /// Returns an error if the signature is not valid, or if the given public key isn't part of - /// the set of signers, or if we already have a different partial signature on-file for that - /// signer. + /// the set of signers, or if the server already have a different partial signature on-file for + /// that signer. fn receive_signature( &mut self, pubkey: XOnlyPublicKey, @@ -187,41 +212,49 @@ pub trait Musig2SignerSecondRound: Send + Sync { /// combining signatures into an aggregated signature on the `message` /// given in the first round finalization. /// - /// This method should only be invoked once [`is_complete`][Self::is_complete] - /// returns true, otherwise it will fail. Can also return an error if partial - /// signature aggregation fails, but if [`receive_signature`][Self::receive_signature] - /// didn't complain, then finalizing will succeed with overwhelming probability. + /// # Warning + /// + /// This method should only be invoked once + /// [`is_complete`][Musig2SignerSecondRound::is_complete] returns true, otherwise it will + /// fail. + /// + /// Can also return an error if partial signature aggregation fails, but if + /// [`receive_signature`][Musig2SignerSecondRound::receive_signature] was successful, then + /// finalizing will succeed with overwhelming probability. fn finalize( self, ) -> impl Future>> + Send; } -/// Winternitz One-Time Signatures are used to transfer state across UTXOs, even though -/// bitcoin does not support this natively. This signer returns deterministic keys so the -/// caller can assemble a transaction. +/// Winternitz One-Time Signatures (WOTS) are used to transfer state across UTXOs, even though +/// bitcoin does not support this natively. +/// +/// This signer returns deterministic keys so the caller can assemble a transaction. pub trait WotsSigner: Send { - /// Returns a deterministic key usable for signing 160 bits of data, with 20 bytes per bit. + /// Returns a deterministic key usable for signing 160 bits of data, with 20 bytes per bit; + /// given a transaction ID, vout, and WOTS index. fn get_160_key( &self, - index: u32, - vout: u32, txid: Txid, + vout: u32, + index: u32, ) -> impl Future> + Send; - /// Returns a key usable for signing 256 bits of data, with 20 bytes per bit. + /// Returns a key usable for signing 256 bits of data, with 20 bytes per bit; + /// given a transaction ID, vout, and WOTS index. fn get_256_key( &self, - index: u32, - vout: u32, txid: Txid, + vout: u32, + index: u32, ) -> impl Future> + Send; } -/// The stakechain preimages struct is used to generate deterministic preimages for the stakechain -/// used for withdrawals. +/// The Stake Chain preimages are used to generate deterministic preimages for the Stake Chain +/// used to advance the operator's stake while fulfilling withdrawals. pub trait StakeChainPreimages: Send { - /// Returns a deterministic preimage for a given stakechain withdrawal through a given txid, - /// vout and stake index. + /// Returns a deterministic preimage for a given stakechain withdrawal through a given pre-stake + /// txid, and vout; and stake index. fn get_preimg( &self, prestake_txid: Txid, @@ -230,12 +263,11 @@ pub trait StakeChainPreimages: Send { ) -> impl Future> + Send; } -/// The origin trait is used to parameterize the main secret service traits so -/// that clients and servers alike can implement a single trait, but clients -/// will receive the server's response wrapped in a result with other spurious -/// network or protocol errors it may encounter. +/// Parameterizes the main secret service traits so that clients and servers alike can implement a +/// single trait, but clients will receive the server's response wrapped in a result with other +/// spurious network or protocol errors it may encounter. pub trait Origin { - /// Container type for responses from secret service traits + /// Container type for responses from secret service traits. type Container; } @@ -248,35 +280,43 @@ impl Origin for Server { type Container = T; } -/// Enforcer for other traits to ensure implementations only work for the client & provides -/// container type -#[derive(Debug)] +/// Enforcer for other traits to ensure implementations only work for the client and provides +/// container type. +#[derive(Debug, Clone)] pub struct Client; impl Origin for Client { - // for the client, we wrap responses in a result that may have a client error + // For the client, the server wrap responses in a result that may have a client error. type Container = Result; } -/// Various errors a client may encounter when interacting with the secret service +/// Various errors a client may encounter when interacting with the Secret Service. #[derive(Debug)] pub enum ClientError { - /// Connection was lost or had an error + /// Connection was lost or had an error. ConnectionError(ConnectionError), - /// Unusual: rkyv failed to serialize something. Indicates something very bad has happened. + + /// Unusual: `rkyv` failed to serialize something. Indicates something very bad has happened. SerializationError(rancor::Error), - /// rkyv failed to deserialize something. Something's probably weird on the - /// server side + + /// `rkyv` failed to deserialize something. Something's probably weird on the + /// server side. DeserializationError(rancor::Error), - /// We failed to deserialize something. Server is giving us bad responses + + /// Failed to deserialize something. Server is giving us bad responses. BadData, - /// We failed to write data towards the server + + /// Failed to write data towards the server. WriteError(WriteError), - /// We failed to read data from the server + + /// Failed to read data from the server. ReadError(ReadExactError), - /// The server took too long to respond + + /// The server took too long to respond. Timeout, - /// The server sent a message that was not expected + + /// The server sent a message that was not expected. WrongMessage(ServerMessage), - /// The server sent a message with an unexpected protocol version + + /// The server sent a message with an unexpected protocol version. WrongVersion, } diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index bca0ddf7..b6b3a9ca 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -1,4 +1,5 @@ //! V1 wire protocol + use bitcoin::{ hashes::Hash, taproot::{ControlBlock, TaprootError}, @@ -15,119 +16,153 @@ use super::traits::{Musig2SessionId, SignerIdxOutOfBounds}; pub enum ServerMessage { /// The message the client sent was invalid. InvalidClientMessage, + /// The server experienced an unexpected internal error while handling the - /// request. Check the server logs for debugging details. + /// request. + /// + /// Check the server logs for debugging details. OpaqueServerError, - /// Response for OperatorSigner::sign + /// Response for [`OperatorSigner::sign`](super::traits::OperatorSigner::sign). OperatorSign { - /// Schnorr signature of provided digest + /// Schnorr signature for a certain message. sig: [u8; 64], }, - /// Response for OperatorSigner::pubkey + + /// Response for [`OperatorSigner::pubkey`](super::traits::OperatorSigner::pubkey). OperatorPubkey { - /// Serialized Schnorr compressed public key for operator signatures + /// Serialized Schnorr [`XOnlyPublicKey`] for operator signatures. pubkey: [u8; 32], }, - /// Response for P2PSigner::sign + /// Response for [`P2PSigner::sign`](super::traits::P2PSigner::sign). P2PSign { - /// Schnorr signature of provided digest + /// Schnorr signature of for a certain message. sig: [u8; 64], }, - /// Response for P2PSigner::pubkey + + /// Response for [`P2PSigner::pubkey`](super::traits::P2PSigner::pubkey). P2PPubkey { - /// Serialized Schnorr compressed public key for P2P signatures + /// Serialized Schnorr [`XOnlyPublicKey`] for P2P signatures. pubkey: [u8; 32], }, - /// Response for Musig2Signer::new_session + /// Response for [`Musig2Signer::new_session`](super::traits::Musig2Signer::new_session). Musig2NewSession(Result), - /// Response for Musig2Signer::pubkey + + /// Response for [`Musig2Signer::pubkey`](super::traits::Musig2Signer::pubkey). Musig2Pubkey { - /// Serialized Schnorr compressed public key for Musig2 signatures + /// Serialized Schnorr [`XOnlyPublicKey`] for MuSig2 signatures. pubkey: [u8; 32], }, - /// Response for Musig2SignerFirstRound::our_nonce + /// Response for + /// [`Musig2SignerFirstRound::our_nonce`](super::traits::Musig2SignerFirstRound::our_nonce). Musig2FirstRoundOurNonce { - /// Our serialized musig2 public nonce for the requested signing session + /// Our serialized MuSig2 public nonce for the requested signing session. our_nonce: [u8; 66], }, - /// Response for Musig2SignerFirstRound::holdouts + + /// Response for + /// [`Musig2SignerFirstRound::holdouts`](super::traits::Musig2SignerFirstRound::holdouts). Musig2FirstRoundHoldouts { - /// Serialized Schnorr compressed public keys of signers whose pub nonces - /// we do not have + /// Serialized Schnorr [`XOnlyPublicKey`] of signers whose public nonces + /// we do not have. pubkeys: Vec<[u8; 32]>, }, - /// Response for Musig2SignerFirstRound::is_complete + /// Response for + /// [`Musig2SignerFirstRound::is_complete`](super::traits::Musig2SignerFirstRound::is_complete). Musig2FirstRoundIsComplete { - /// What do you think it means? + /// Flag indicating whether the MuSig2 first round is complete. complete: bool, }, - /// Response for Musig2SignerFirstRound::receive_pub_nonce + + /// Response for + /// [`Musig2SignerFirstRound::receive_pub_nonce`](super::traits::Musig2SignerFirstRound::receive_pub_nonce). Musig2FirstRoundReceivePubNonce( + /// Error indicating whether the server was unable to process the request. #[rkyv(with = Map)] Option, ), - /// Response for Musig2SignerFirstRound::finalize + + /// Response for + /// [`Musig2SignerFirstRound::finalize`](super::traits::Musig2SignerFirstRound::finalize). Musig2FirstRoundFinalize( - #[rkyv(with = Map)] Option, + /// Error indicating whether the server was unable to process the request. + #[rkyv(with = Map)] + Option, ), - /// Response for Musig2SignerSecondRound::agg_nonce + /// Response for + /// [`Musig2SignerSecondRound::agg_nonce`](super::traits::Musig2SignerSecondRound::agg_nonce). Musig2SecondRoundAggNonce { - /// Serialized aggregated nonce of the signing session's first round + /// Serialized aggregated public nonce of the signing session's first round. nonce: [u8; 66], }, - /// Response for Musig2SignerSecondRound::holdouts + + /// Response for + /// [`Musig2SignerSecondRound::holdouts`](super::traits::Musig2SignerSecondRound::holdouts). Musig2SecondRoundHoldouts { - /// Serialized Schnorr compressed public keys of signers whose partial signatures - /// we do not have for this signing session + /// Serialized Schnorr [`XOnlyPublicKey`] of signers whose partial signatures + /// we do not have for this signing session. pubkeys: Vec<[u8; 32]>, }, - /// Response for Musig2SignerSecondRound::our_signature + + /// Response for + /// [`Musig2SignerSecondRound::our_signature`](super::traits::Musig2SignerSecondRound::our_signature). Musig2SecondRoundOurSignature { - /// Our serialized partial signature of the signing session + /// This server's serialized partial signature of the signing session. sig: [u8; 32], }, - /// Response for Musig2SignerSecondRound::is_complete + + /// Response for + /// [`Musig2SignerSecondRound::is_complete`](super::traits::Musig2SignerSecondRound::is_complete). Musig2SecondRoundIsComplete { - /// Hmm. I wonder what this could mean. + /// Flag indicating whether the MuSig2 second round is complete. complete: bool, }, - /// Response for Musig2SignerSecondRound::receive_signature + + /// Response for + /// [`Musig2SignerSecondRound::receive_signature`](super::traits::Musig2SignerSecondRound::receive_signature). Musig2SecondRoundReceiveSignature( + /// The error that occurred during the signature reception. #[rkyv(with = Map)] Option, ), - /// Response for Musig2SignerSecondRound::finalize + + /// Response for + /// [`Musig2SignerSecondRound::finalize`](super::traits::Musig2SignerSecondRound::finalize). Musig2SecondRoundFinalize(Musig2SessionResult), - /// Response for WotsSigner::get_160_key + /// Response for [`WotsSigner::get_160_key`](super::traits::WotsSigner::get_160_key). WotsGet160Key { - /// A set of 20 byte keys, one for each bit + /// A set of 20 byte keys, one for each bit that is committed to. key: [u8; 20 * 160], }, - /// Response for WotsSigner::get_256_key + + /// Response for [`WotsSigner::get_256_key`](super::traits::WotsSigner::get_256_key). WotsGet256Key { - /// A set of 20 byte keys, one for each bit + /// A set of 20 byte keys, one for each bit that is committed to. key: [u8; 20 * 256], }, - /// Response for StakeChainPreimages::get_preimg + /// Response for + /// [`StakeChainPreimages::get_preimg`](super::traits::StakeChainPreimages::get_preimg). StakeChainGetPreimage { - /// The preimage you asked for? + /// The preimage that was requested. preimg: [u8; 32], }, } -/// Helper type for serialization -/// Maybe replaced with a future rkyv::with::MapRes or smth? +/// Helper type for serialization. +// TODO: Maybe replaced with a future rkyv::with::MapRes or smth? #[allow(missing_docs)] #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum Musig2SessionResult { + /// The result of a MuSig2 session. Ok([u8; 64]), + + /// The error that occurred during a MuSig2 session. Err(#[rkyv(with = super::rkyv_wrappers::RoundFinalizeError)] RoundFinalizeError), } @@ -149,151 +184,203 @@ impl From for Result<[u8; 64], RoundFinalizeError> { } } -// impl> WireMessageMarker for ServerMessage {} - /// Various messages the client can send to the server. #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum ClientMessage { - /// Request for OperatorSigner::sign + /// Request for [`OperatorSigner::sign`](super::traits::OperatorSigner::sign). OperatorSign { - /// The digest of the data we want signed + /// The digest of the data the client wants signed. digest: [u8; 32], }, - /// Request for OperatorSigner::pubkey + + /// Request for [`OperatorSigner::pubkey`](super::traits::OperatorSigner::pubkey). OperatorPubkey, - /// Request for P2PSigner::sign + /// Request for [`P2PSigner::sign`](super::traits::P2PSigner::sign). P2PSign { - /// The digest of the data we want signed + /// The digest of the data the client wants signed. digest: [u8; 32], }, - /// Request for P2PSigner::pubkey + + /// Request for [`P2PSigner::pubkey`](super::traits::P2PSigner::pubkey). P2PPubkey, - /// Request for Musig2Signer::new_session + /// Request for [`Musig2Signer::new_session`](super::traits::Musig2Signer::new_session). Musig2NewSession { /// Public keys for the signing session. May or may not include our own /// public key. If not present, it should be added. May or may not be sorted. pubkeys: Vec<[u8; 32]>, + /// The taproot witness of the input witness: SerializableTaprootWitness, - /// Serialized txid of the input tx + + /// Serialized [`Txid`](bitcoin::Txid) of the input transaction ID. input_txid: [u8; 32], - /// The vout of the input tx we're signing for (i think?) + + /// The vout of the input transaction the client is signing for. input_vout: u32, }, - /// Request for Musig2Signer::pubkey + + /// Request for [`Musig2Signer::pubkey`](super::traits::Musig2Signer::pubkey). Musig2Pubkey, - /// Request for Musig2SignerFirstRound::our_nonce + /// Request for + /// [`Musig2SignerFirstRound::our_nonce`](super::traits::Musig2SignerFirstRound::our_nonce). Musig2FirstRoundOurNonce { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerFirstRound::holdouts + + /// Request for + /// [`Musig2SignerFirstRound::holdouts`](super::traits::Musig2SignerFirstRound::holdouts) Musig2FirstRoundHoldouts { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerFirstRound::is_complete + + /// Request for + /// [`Musig2SignerFirstRound::is_complete`](super::traits::Musig2SignerFirstRound::is_complete). Musig2FirstRoundIsComplete { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerFirstRound::receive_pub_nonce + + /// Request for + /// [`Musig2SignerFirstRound::receive_pub_nonce`](super::traits::Musig2SignerFirstRound::receive_pub_nonce). Musig2FirstRoundReceivePubNonce { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, - /// The serialized compressed schnorr pubkey of the signer whose pubnonce this is + + /// The serialized [`XOnlyPublicKey`] of the signer whose public nonce this is. pubkey: [u8; 32], + /// Serialized public nonce pubnonce: [u8; 66], }, - /// Request for Musig2SignerFirstRound::finalize + + /// Request for + /// [`Musig2SignerFirstRound::finalize`](super::traits::Musig2SignerFirstRound::finalize). Musig2FirstRoundFinalize { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, - /// Digest of message we're signing + + /// Digest of message the client is signing. digest: [u8; 32], }, - /// Request for Musig2SignerSecondRound::agg_nonce + /// Request for + /// [`Musig2SignerSecondRound::agg_nonce`](super::traits::Musig2SignerSecondRound::agg_nonce). Musig2SecondRoundAggNonce { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerSecondRound::holdouts + + /// Request for + /// [`Musig2SignerSecondRound::holdouts`](super::traits::Musig2SignerSecondRound::holdouts). Musig2SecondRoundHoldouts { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerSecondRound::our_signature + + /// Request for + /// [`Musig2SignerSecondRound::our_signature`](super::traits::Musig2SignerSecondRound::our_signature). Musig2SecondRoundOurSignature { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerSecondRound::is_complete + + /// Request for + /// [`Musig2SignerSecondRound::is_complete`](super::traits::Musig2SignerSecondRound::is_complete). Musig2SecondRoundIsComplete { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for Musig2SignerSecondRound::receive_signature + + /// Request for + /// [`Musig2SignerSecondRound::receive_signature`](super::traits::Musig2SignerSecondRound::receive_signature). Musig2SecondRoundReceiveSignature { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, - /// The serialized compressed schnorr pubkey of the signer whose pubnonce this is + + /// The serialized [`XOnlyPublicKey`] of the signer whose public nonce this is. pubkey: [u8; 32], - /// That signer's musig2 partial sig + + /// That signer's MuSig2 partial signature. signature: [u8; 32], }, - /// Request for Musig2SignerSecondRound::finalize + + /// Request for + /// [`Musig2SignerSecondRound::finalize`](super::traits::Musig2SignerSecondRound::finalize). Musig2SecondRoundFinalize { - /// Session that we're requesting for + /// Session that this server is requesting for. session_id: usize, }, - /// Request for WotsSigner::get_160_key + /// Request for [`WotsSigner::get_160_key`](super::traits::WotsSigner::get_160_key). WotsGet160Key { - /// Transaction index (?) opaque - index: u32, - /// Transaction vout (?) opaque - vout: u32, - /// Transaction txid (?) opaque + /// [`Txid`](bitcoin::Txid) that this WOTS public key is derived from. txid: [u8; 32], + + /// Transaction's vout that this WOTS public key is derived from. + vout: u32, + + /// Transaction's index that this WOTS public key is derived from. + /// + /// Some inputs ([`Txid`](bitcoin::Txid) and vout) need more than one WOTS public key, + /// hence to resolve the ambiguity, the index is needed. + index: u32, }, - /// Request for WotsSigner::get_256_key + + /// Request for [`WotsSigner::get_256_key`](super::traits::WotsSigner::get_256_key). WotsGet256Key { - /// Transaction index (?) opaque - index: u32, - /// Transaction vout (?) opaque - vout: u32, - /// Transaction txid (?) opaque + /// [`Txid`](bitcoin::Txid) that this WOTS public key is derived from. txid: [u8; 32], + + /// Transaction's vout that this WOTS public key is derived from. + vout: u32, + + /// Transaction's index that this WOTS public key is derived from. + /// + /// Some inputs ([`Txid`](bitcoin::Txid) and vout) need more than one WOTS public key, + /// hence to resolve the ambiguity, the index is needed. + index: u32, }, - /// Request for StakeChainPreimages::get_preimg + /// Request for + /// [`StakeChainPreimages::get_preimg`](super::traits::StakeChainPreimages::get_preimg). StakeChainGetPreimage { - /// Transaction txid (?) opaque + /// The Pre-Stake [`Txid`](bitcoin::Txid) that this Stake Chain preimage is derived from. prestake_txid: [u8; 32], - /// Transaction vout (?) opaque + + /// The Pre-Stake transaction's vout that this Stake Chain preimage is derived from. prestake_vout: u32, - /// Stake index (?) opaque + + /// Stake index that this Stake Chain preimage is derived from. stake_index: u32, }, } -/// Serializable version of [`TaprootWitness`] -#[allow(missing_docs)] +/// Serializable version of [`TaprootWitness`]. #[derive(Debug, Clone, Archive, Serialize, Deserialize)] pub enum SerializableTaprootWitness { + /// Use the keypath spend. + /// + /// This only requires the signature for the tweaked internal key and nothing else. Key, + + /// Use the script path spend. + /// + /// This requires the script being spent from as well as the [`ControlBlock`] in addition to + /// the elements that fulfill the spending condition in the script. Script { + /// Raw bytes of the [`ScriptBuf`]. script_buf: Vec, + /// Raw bytes of the [`ControlBlock`]. control_block: Vec, }, - Tweaked { - tweak: [u8; 32], - }, + + /// Use the keypath spend tweaked with some known hash. + Tweaked { tweak: [u8; 32] }, } impl From for SerializableTaprootWitness { diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs index 44611333..ae5280b7 100644 --- a/crates/secret-service-proto/src/wire.rs +++ b/crates/secret-service-proto/src/wire.rs @@ -1,4 +1,5 @@ -//! Secret service wire protocol +//! Secret Service wire protocol + use rkyv::{ api::high::{to_bytes_in, HighSerializer}, rancor, @@ -15,7 +16,7 @@ trait WireMessageMarker: { } -/// A trait for serializing wire messages +/// A trait for serializing wire messages. pub trait WireMessage { /// Serialize the wire message into an aligned vector using rkyv. fn serialize(&self) -> Result<([u8; 2], AlignedVec), rancor::Error>; diff --git a/crates/secret-service-server/src/bool_arr.rs b/crates/secret-service-server/src/bool_arr.rs index 35644fdc..0d310d17 100644 --- a/crates/secret-service-server/src/bool_arr.rs +++ b/crates/secret-service-server/src/bool_arr.rs @@ -7,6 +7,7 @@ //! over the array for scanning for a slot in a particular state. //! //! # Examples +//! //! ``` //! use std::convert::Infallible; //! @@ -57,6 +58,7 @@ use std::{ /// Compact storage for types representable as two boolean values (four possible states). /// /// Each entry is stored as two bits, with the following mapping: +/// /// - Bit 0: First boolean value (LSB) /// - Bit 1: Second boolean value /// @@ -64,11 +66,13 @@ use std::{ /// IMPORTANT: When T is `(false, false)`, it represents an empty state. /// /// # Type Parameters -/// - `N`: Number of `u64` chunks used for storage (capacity = N × 32) +/// +/// - `N`: Number of `u64` chunks used for storage (capacity = `N × 32`) /// - `T`: Stored type that can be converted to/from `(bool, bool)` pairs /// /// # Implementation Details -/// - Stores values in N `u64` integers (8N bytes total) +/// +/// - Stores values in N `u64` integers (`8N` bytes total) /// - Provides O(1) access time for get/set operations /// - Implements space-efficient storage with 2 bits per entry pub struct DoubleBoolArray([u64; N], PhantomData) @@ -122,12 +126,13 @@ where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, >::Error: Debug, { - /// Returns the capacity of the array in terms of the number of (bool, bool) slots it can hold. + /// Returns the capacity of the array in terms of the number of `(bool, bool)` slots it can + /// hold. pub const fn capacity() -> usize { N * (std::mem::size_of::() * 8 / 2) } - /// Find the index of the first slot with the specified value. + /// Finds the index of the first slot with the specified value. pub fn find_first_slot_with(&self, target: T) -> Option { let (target_0, target_1) = target.into(); let target = (target_0 as u64) | ((target_1 as u64) << 1); @@ -142,8 +147,8 @@ where None } - /// Get the two boolean values at specified index - /// Panics if index >= N * 32 + /// Gets the two boolean values at specified index. + /// Panics if `index >= N * 32`. pub fn get(&self, index: usize) -> T { assert!(index < N * 32, "Index out of bounds"); let chunk_idx = index / 32; @@ -155,8 +160,8 @@ where .expect("T::try_from(T::Into) should always succeed") } - /// Set the two boolean values at specified index - /// Panics if index >= N * 32 + /// Sets the two boolean values at specified index. + /// Panics if `index >= N * 32`. pub fn set(&mut self, index: usize, value: T) { assert!(index < N * 32, "Index out of bounds"); let chunk_idx = index / 32; diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index c77a9302..242f7f20 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -1,4 +1,5 @@ //! This module contains the implementation of the secret service server. +//! //! This handles networking and communication with clients, but does not implement the traits //! for the secret service protocol. @@ -42,13 +43,15 @@ use tracing::{error, span, warn, Instrument, Level}; pub struct Config { /// The address to bind the server to. pub addr: SocketAddr, + /// The maximum number of concurrent connections allowed. pub connection_limit: Option, + /// The TLS configuration for the server. pub tls_config: rustls::ServerConfig, } -/// Run the secret service server given the service and a server configuration. +/// Runs the secret service server given the service and a server configuration. pub async fn run_server( c: Config, service: Arc, @@ -83,6 +86,7 @@ where Ok(()) } +/// Handles a single incoming connection. async fn conn_handler( incoming: Incoming, service: Arc, @@ -128,6 +132,7 @@ async fn conn_handler( } } +/// Manages the stream of requests. async fn request_manager( mut tx: SendStream, handler: JoinHandle>, @@ -162,6 +167,7 @@ async fn request_manager( } } +/// Manages the stream of requests. async fn request_handler( mut rx: RecvStream, service: Arc, @@ -257,6 +263,7 @@ where ServerMessage::Musig2NewSession(Ok(write_perm.session_id())) } + ArchivedClientMessage::Musig2Pubkey => ServerMessage::Musig2Pubkey { pubkey: service.musig2_signer().pubkey().await.serialize(), }, @@ -274,6 +281,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2FirstRoundHoldouts { session_id } => { let r = musig2_sm .lock() @@ -293,6 +301,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2FirstRoundIsComplete { session_id } => { let r = musig2_sm .lock() @@ -305,6 +314,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2FirstRoundReceivePubNonce { session_id, pubkey, @@ -323,6 +333,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2FirstRoundFinalize { session_id, digest } => { let session_id = session_id.to_native() as usize; let mut sm = musig2_sm.lock().await; @@ -378,6 +389,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2SecondRoundOurSignature { session_id } => { let sr = musig2_sm .lock() @@ -391,6 +403,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2SecondRoundIsComplete { session_id } => { let sr = musig2_sm .lock() @@ -404,6 +417,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2SecondRoundReceiveSignature { session_id, pubkey, @@ -422,6 +436,7 @@ where _ => ServerMessage::InvalidClientMessage, } } + ArchivedClientMessage::Musig2SecondRoundFinalize { session_id } => { let r = musig2_sm .lock() @@ -439,15 +454,16 @@ where let txid = Txid::from_slice(txid).expect("correct length"); let key = service .wots_signer() - .get_160_key(index.into(), vout.into(), txid) + .get_160_key(txid, vout.into(), index.into()) .await; ServerMessage::WotsGet160Key { key } } + ArchivedClientMessage::WotsGet256Key { index, vout, txid } => { let txid = Txid::from_slice(txid).expect("correct length"); let key = service .wots_signer() - .get_256_key(index.into(), vout.into(), txid) + .get_256_key(txid, vout.into(), index.into()) .await; ServerMessage::WotsGet256Key { key } } diff --git a/crates/secret-service-server/src/musig2_session_mgr.rs b/crates/secret-service-server/src/musig2_session_mgr.rs index 66db018b..0a99e18c 100644 --- a/crates/secret-service-server/src/musig2_session_mgr.rs +++ b/crates/secret-service-server/src/musig2_session_mgr.rs @@ -1,6 +1,6 @@ -//! This module contains the Musig2SessionManager which manages in-memory Musig2 +//! This module contains the Musig2SessionManager which manages in-memory MuSig2 //! sessions globally for a given server. This allows ergonomic (and correct) usage -//! of S2's musig2 features. +//! of Secret Service's musig2 features. use std::{mem::MaybeUninit, sync::Arc}; @@ -11,8 +11,8 @@ use tokio::sync::{Mutex, MutexGuard}; use crate::bool_arr::DoubleBoolArray; -/// Musig2SessionManager is responsible for tracking and managing secret service -/// musig2 sessions. +/// [`Musig2SessionManager`] is responsible for tracking and managing Secret Service's +/// MuSig2 sessions. #[derive(Debug)] pub struct Musig2SessionManager where @@ -20,15 +20,25 @@ where FirstRound: Musig2SignerFirstRound, { /// Tracker is used for tracking whether a session is in first round, - /// second round or completed. N=128 means we can track 128*32=4096 sessions + /// second round or completed. + /// + /// Example: when `N=128` means we can track `128 * 32 = 4_096` sessions. tracker: DoubleBoolArray, - /// Used to store first rounds of musig2 server instances. This is a Vec - /// because we don't know how big FirstRound may be in memory so we will - /// heap allocate and try keep this to a minimum + + /// Used to store first rounds of musig2 server instances. + /// + /// # Implementation Details + /// + /// This is a [`Vec`] because the server doesn't know how big `FirstRound` may be in memory + /// so it will heap allocate and try keep this to a minimum. first_rounds: Vec>>>, - /// Used to store second rounds of musig2 server instances. This is a Vec - /// because we don't know how big SecondRound may be in memory so we will - /// heap allocate and try keep this to a minimum + + /// Used to store second rounds of MuSig2 server instances. + /// + /// # Implementation Details + /// + /// This is a [`Vec`] because the server doesn't know how big `SecondRound` may be in memory so + /// it will heap allocate and try keep this to a minimum. second_rounds: Vec>>>, } @@ -47,29 +57,31 @@ where } } -/// The provided session index is out of range +/// The provided session index is out of range. #[derive(Debug)] pub struct OutOfRange; -/// The session manager is full and cannot accept any more sessions +/// The session manager is full and cannot accept any more sessions. #[derive(Debug)] pub struct Full; -/// The session was assumed to be in a round that it was not in +/// The session was assumed to be in a round that it was not in. #[derive(Debug)] pub struct NotInCorrectRound { - /// The state the session was assumed to be in + /// The state the session was assumed to be in. pub wanted: SlotState, - /// The state the session was actually in + + /// The state the session was actually in. pub got: SlotState, } -/// We couldn't take ownership of the session because something else was still +/// The server couldn't take ownership of the session because something else was still /// using it. Try again. #[derive(Debug)] pub struct OtherReferencesActive; -/// A struct representing a permission from the session manager to write to a given slot. +/// Permission from the session manager to write to a given slot. +/// /// This allows inspection of the allocated session ID and value before it is transferred /// to the session manager's ownership. #[derive(Debug)] @@ -80,7 +92,7 @@ pub struct WritePermission<'a, T> { } impl WritePermission<'_, T> { - /// Returns a reference to the value inside the mutex. + /// Returns a reference to the value inside the Mutex. pub async fn value(&self) -> MutexGuard<'_, T> { self.t.lock().await } @@ -134,7 +146,7 @@ where } } - /// Attempts to transition a musig2 session from the first round by + /// Attempts to transition a MuSig2 session from the first round by /// finalizing it. pub async fn transition_first_to_second_round( &mut self, @@ -181,7 +193,7 @@ where } } - /// Attempts to finalize the second round of a musig2 session. + /// Attempts to finalize the second round of a MuSig2 session. pub async fn finalize_second_round( &mut self, session_id: usize, @@ -224,7 +236,7 @@ where } } - /// Attempts to retrieve the first round of a musig2 session. + /// Attempts to retrieve the first round of a MuSig2 session. pub fn first_round( &self, session_id: usize, @@ -238,7 +250,7 @@ where } } - /// Attempts to retrieve the second round of a musig2 session. + /// Attempts to retrieve the second round of a MuSig2 session. pub fn second_round( &self, session_id: usize, @@ -253,15 +265,18 @@ where } } -/// Represents the state of a slot in the musig2 session manager. Used with the -/// bool_arr to improve scan performance. +/// Represents the state of a slot in the MuSig2 session manager. +/// +/// Used with the [`bool_arr`](crate::bool_arr) to improve scan performance. #[derive(Debug)] pub enum SlotState { - /// There's no musig2 session in this slot. + /// There's no MuSig2 session in this slot. Empty, - /// There's a musig2 session in this slot in its first round stage. + + /// There's a MuSig2 session in this slot in its first round stage. FirstRound, - /// There's a musig2 session in this slot in its second round stage. + + /// There's a MuSig2 session in this slot in its second round stage. SecondRound, } diff --git a/crates/secret-service/src/config.rs b/crates/secret-service/src/config.rs index 8d0c0582..4be8a96a 100644 --- a/crates/secret-service/src/config.rs +++ b/crates/secret-service/src/config.rs @@ -1,14 +1,26 @@ +//! Configuration for the Secret Service. + use std::{net::SocketAddr, path::PathBuf}; -#[derive(serde::Deserialize)] +use serde::Deserialize; + +/// Configuration for the Secret Service. +/// +/// It is parsed from a TOML file. +#[derive(Debug, Deserialize)] pub struct TomlConfig { + /// Configuration for TLS. pub tls: TlsConfig, + + /// Configuration for the transport layer. pub transport: TransportConfig, - /// A file path to a 32 byte seed file. + + /// A file path to a 32-byte seed file. pub seed: Option, } -#[derive(serde::Deserialize)] +/// Configuration for the transport layer. +#[derive(Debug, Deserialize)] pub struct TransportConfig { /// Address to listen on for incoming connections. pub addr: SocketAddr, @@ -17,13 +29,13 @@ pub struct TransportConfig { } /// Configuration for TLS. -#[derive(serde::Deserialize)] +#[derive(Debug, Deserialize)] pub struct TlsConfig { /// Path to the certificate file. pub cert: Option, /// Path to the private key file. pub key: Option, /// Path to the CA certificate to verify client certificates against. - /// Note that S2 is insecure without client authentication. + /// Note that Secret Service is insecure without client authentication. pub ca: Option, } diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 56fe3d14..00a2bcf9 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -1,3 +1,5 @@ +//! In-memory persistence for the Secret Service. + use std::path::Path; use bitcoin::{bip32::Xpriv, Network}; @@ -19,13 +21,17 @@ pub mod p2p; pub mod stakechain; pub mod wots; +/// Secret data for the Secret Service. +#[derive(Debug)] pub struct Service { + /// Operator's keys. keys: OperatorKeys, } const NETWORK: Network = Network::Signet; impl Service { + /// Loads the operator's keys from a seed file. pub async fn load_from_seed(seed_path: &Path) -> io::Result { let mut seed = [0; 32]; diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index ea264a89..3429bbb9 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -1,3 +1,5 @@ +//! In-memory persistence for MuSig2's secret data. + use std::future::Future; use bitcoin::{ @@ -20,12 +22,18 @@ use secret_service_proto::v1::traits::{ use sha2::Sha256; use strata_bridge_primitives::scripts::taproot::TaprootWitness; +/// Secret data for the MuSig2 signer. +#[derive(Debug)] pub struct Ms2Signer { + /// Operator's [`Keypair`]. kp: Keypair, + + /// Initial key material to derive secret nonces. ikm: [u8; 32], } impl Ms2Signer { + /// Creates a new MuSig2 signer given a master [`Xpriv`]. pub fn new(base: &Xpriv) -> Self { let key = base .derive_priv( @@ -122,9 +130,16 @@ impl Musig2Signer for Ms2Signer { } } +/// First round of the MuSig2 protocol for the server. +#[allow(clippy::missing_debug_implementations)] pub struct ServerFirstRound { + /// The first round of the MuSig2 protocol. first_round: FirstRound, + + /// Lexicographically-sorted X-only public keys of the signers. ordered_public_keys: Vec, + + /// Operator's [`SecretKey`]. seckey: SecretKey, } @@ -184,8 +199,13 @@ impl Musig2SignerFirstRound for ServerFirstRound { } } +/// Second round of the MuSig2 protocol for the server. +#[allow(clippy::missing_debug_implementations)] pub struct ServerSecondRound { + /// The second round of the MuSig2 protocol. second_round: SecondRound<[u8; 32]>, + + /// Lexicographically-sorted X-only public keys of the signers. ordered_public_keys: Vec, } diff --git a/crates/secret-service/src/disk/operator.rs b/crates/secret-service/src/disk/operator.rs index be74fb9b..dfd2aea5 100644 --- a/crates/secret-service/src/disk/operator.rs +++ b/crates/secret-service/src/disk/operator.rs @@ -1,14 +1,20 @@ +//! In-memory persistence for operator's secret data. + use std::future::Future; use bitcoin::{key::Keypair, XOnlyPublicKey}; use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{OperatorSigner, Origin, Server}; +/// Secret data for the operator. +#[derive(Debug)] pub struct Operator { + /// Operator's [`Keypair`] for signing and verifying messages. kp: Keypair, } impl Operator { + /// Create a new operator with the given secret key. pub fn new(sk: SecretKey) -> Self { let kp = Keypair::from_secret_key(SECP256K1, &sk); Self { kp } diff --git a/crates/secret-service/src/disk/p2p.rs b/crates/secret-service/src/disk/p2p.rs index 3f040f36..c66fb9ed 100644 --- a/crates/secret-service/src/disk/p2p.rs +++ b/crates/secret-service/src/disk/p2p.rs @@ -1,14 +1,20 @@ +//! In-memory persistence for operator's P2P secret data. + use std::future::Future; use bitcoin::{key::Keypair, XOnlyPublicKey}; use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{P2PSigner, Server}; +/// Secret data for the P2P signer. +#[derive(Debug)] pub struct ServerP2PSigner { + /// The [`Keypair`] for the P2P signer. kp: Keypair, } impl ServerP2PSigner { + /// Creates a new [`ServerP2PSigner`] with the given secret key. pub fn new(sk: SecretKey) -> Self { let kp = Keypair::from_secret_key(SECP256K1, &sk); Self { kp } diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs index ba66698a..fe621685 100644 --- a/crates/secret-service/src/disk/stakechain.rs +++ b/crates/secret-service/src/disk/stakechain.rs @@ -1,3 +1,5 @@ +//! In-memory persistence for Stake Chain preimages. + use std::future::Future; use bitcoin::{ @@ -11,16 +13,21 @@ use musig2::secp256k1::SECP256K1; use secret_service_proto::v1::traits::{Server, StakeChainPreimages}; use sha2::Sha256; +/// Secret data for the Stake Chain preimages. +#[derive(Debug)] pub struct StakeChain { + /// The initial key material to derive Stake Chain preimages. ikm: [u8; 32], } impl StakeChain { + /// Creates a new [`StakeChain`] given a master [`Xpriv`]. pub fn new(base: &Xpriv) -> Self { let xpriv = base .derive_priv( SECP256K1, &[ + // TODO: move to constants. ChildNumber::from_hardened_idx(80).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), ], @@ -33,6 +40,8 @@ impl StakeChain { } impl StakeChainPreimages for StakeChain { + /// Gets a preimage for a Stake Chain, given a pre-stake transaction ID, and output index; and + /// stake index. fn get_preimg( &self, prestake_txid: Txid, diff --git a/crates/secret-service/src/disk/wots.rs b/crates/secret-service/src/disk/wots.rs index 23a8cc55..e12c2df9 100644 --- a/crates/secret-service/src/disk/wots.rs +++ b/crates/secret-service/src/disk/wots.rs @@ -1,3 +1,5 @@ +//! In-memory persistence for the Winternitz One-Time Signature (WOTS) keys. + use std::future::Future; use bitcoin::{ @@ -28,6 +30,7 @@ impl SeededWotsSigner { .derive_priv( SECP256K1, &[ + // TODO: move to constants. ChildNumber::from_hardened_idx(79).unwrap(), ChildNumber::from_hardened_idx(160).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), @@ -40,6 +43,7 @@ impl SeededWotsSigner { .derive_priv( SECP256K1, &[ + // TODO: move to constants. ChildNumber::from_hardened_idx(79).unwrap(), ChildNumber::from_hardened_idx(256).unwrap(), ChildNumber::from_hardened_idx(0).unwrap(), @@ -55,9 +59,9 @@ impl SeededWotsSigner { impl WotsSigner for SeededWotsSigner { fn get_160_key( &self, - index: u32, - vout: u32, txid: Txid, + vout: u32, + index: u32, ) -> impl Future + Send { async move { let hk = Hkdf::::new(None, &self.ikm_160); @@ -74,9 +78,9 @@ impl WotsSigner for SeededWotsSigner { fn get_256_key( &self, - index: u32, - vout: u32, txid: Txid, + vout: u32, + index: u32, ) -> impl Future + Send { async move { let hk = Hkdf::::new(None, &self.ikm_256); diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index df5b17a9..0471009f 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,3 +1,5 @@ +//! Runs the Secret Service. + // use secret_service_server::rustls::ServerConfig; pub mod config; @@ -15,8 +17,10 @@ use secret_service_server::{run_server, Config}; use tls::load_tls; use tracing::{info, warn, Level}; +/// Runs the Secret Service in development mode if the `SECRET_SERVICE_DEV` environment variable is +/// set to `1`. pub static DEV_MODE: LazyLock = - LazyLock::new(|| std::env::var("S2_DEV").is_ok_and(|v| &v == "1")); + LazyLock::new(|| std::env::var("SECRET_SERVICE_DEV").is_ok_and(|v| &v == "1")); #[tokio::main] async fn main() { diff --git a/crates/secret-service/src/tls.rs b/crates/secret-service/src/tls.rs index 0650b833..57d3d88d 100644 --- a/crates/secret-service/src/tls.rs +++ b/crates/secret-service/src/tls.rs @@ -1,3 +1,5 @@ +//! TLS-related Secret Service functionality. + use std::path::PathBuf; use secret_service_server::rustls::{ @@ -10,6 +12,7 @@ use tracing::{error, info, warn}; use crate::{config::TlsConfig, DEV_MODE}; +/// Loads a TLS configuration for the Secret Service server. pub async fn load_tls(conf: TlsConfig) -> ServerConfig { let (certs, key) = if let (Some(crt_path), Some(key_path)) = (conf.cert, conf.key) { let key = fs::read(&key_path).await.expect("readable key"); @@ -59,6 +62,7 @@ pub async fn load_tls(conf: TlsConfig) -> ServerConfig { .expect("valid rustls config") } +/// Reads a certificate from a file. async fn read_cert(path: PathBuf) -> io::Result>> { let cert_chain = fs::read(&path).await?; if path.extension().is_some_and(|x| x == "der") { From fb598ec76621be0269665523ac5348f316419c59 Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 24 Feb 2025 12:56:00 +0000 Subject: [PATCH 23/30] fix lint warnings --- crates/secret-service-client/src/lib.rs | 10 +++++----- crates/secret-service-client/src/musig2.rs | 1 - crates/secret-service-client/src/operator.rs | 2 +- crates/secret-service-client/src/p2p.rs | 2 +- crates/secret-service-proto/src/lib.rs | 1 + crates/secret-service-proto/src/v1/wire.rs | 5 ++++- crates/secret-service-proto/src/wire.rs | 6 +----- crates/secret-service-server/src/lib.rs | 3 +-- crates/secret-service/src/disk/mod.rs | 1 + crates/secret-service/src/disk/musig2.rs | 6 +++--- crates/secret-service/src/disk/operator.rs | 2 +- crates/secret-service/src/disk/p2p.rs | 2 +- crates/secret-service/src/tests.rs | 2 +- crates/secret-service/src/tls.rs | 2 +- 14 files changed, 22 insertions(+), 23 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index d133b8bf..37e3f993 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -43,23 +43,23 @@ use wots::WotsClient; #[derive(Clone, Debug)] pub struct Config { /// Server to connect to. - server_addr: SocketAddr, + pub server_addr: SocketAddr, /// Hostname present on the server's certificate. - server_hostname: String, + pub server_hostname: String, /// Optional local socket to connect via. - local_addr: Option, + pub local_addr: Option, /// Config for TLS. /// /// # Warning /// /// Users should always be verifying the server's identity via this to prevent MITM attacks. - tls_config: rustls::ClientConfig, + pub tls_config: rustls::ClientConfig, /// Timeout for requests. - timeout: Duration, + pub timeout: Duration, } /// A client that connects to a remote secret service via QUIC. diff --git a/crates/secret-service-client/src/musig2.rs b/crates/secret-service-client/src/musig2.rs index fecf73fe..1f749f85 100644 --- a/crates/secret-service-client/src/musig2.rs +++ b/crates/secret-service-client/src/musig2.rs @@ -5,7 +5,6 @@ use std::{future::Future, sync::Arc}; use bitcoin::{hashes::Hash, Txid, XOnlyPublicKey}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::PublicKey, AggNonce, LiftedSignature, PubNonce, }; use quinn::Connection; diff --git a/crates/secret-service-client/src/operator.rs b/crates/secret-service-client/src/operator.rs index 6d07bfc5..64d1c5f1 100644 --- a/crates/secret-service-client/src/operator.rs +++ b/crates/secret-service-client/src/operator.rs @@ -3,7 +3,7 @@ use std::{future::Future, sync::Arc}; use bitcoin::XOnlyPublicKey; -use musig2::secp256k1::{schnorr::Signature, PublicKey}; +use musig2::secp256k1::schnorr::Signature; use quinn::Connection; use secret_service_proto::v1::{ traits::{Client, ClientError, OperatorSigner, Origin}, diff --git a/crates/secret-service-client/src/p2p.rs b/crates/secret-service-client/src/p2p.rs index ddad6e4d..d17191a9 100644 --- a/crates/secret-service-client/src/p2p.rs +++ b/crates/secret-service-client/src/p2p.rs @@ -3,7 +3,7 @@ use std::{future::Future, sync::Arc}; use bitcoin::XOnlyPublicKey; -use musig2::secp256k1::{schnorr::Signature, PublicKey}; +use musig2::secp256k1::schnorr::Signature; use quinn::Connection; use secret_service_proto::v1::{ traits::{Client, ClientError, Origin, P2PSigner}, diff --git a/crates/secret-service-proto/src/lib.rs b/crates/secret-service-proto/src/lib.rs index a9a89841..910fc6b4 100644 --- a/crates/secret-service-proto/src/lib.rs +++ b/crates/secret-service-proto/src/lib.rs @@ -1,4 +1,5 @@ //! Protocol definitions for the Secret Service. +#[allow(missing_docs)] // because lints wouldn't shut up with rkyv's Archive proc macro pub mod v1; pub mod wire; diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index b6b3a9ca..21ceb3d9 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -380,7 +380,10 @@ pub enum SerializableTaprootWitness { }, /// Use the keypath spend tweaked with some known hash. - Tweaked { tweak: [u8; 32] }, + Tweaked { + /// Tagged hash used in taproot trees. + tweak: [u8; 32], + }, } impl From for SerializableTaprootWitness { diff --git a/crates/secret-service-proto/src/wire.rs b/crates/secret-service-proto/src/wire.rs index ae5280b7..6b87e150 100644 --- a/crates/secret-service-proto/src/wire.rs +++ b/crates/secret-service-proto/src/wire.rs @@ -1,11 +1,7 @@ //! Secret Service wire protocol use rkyv::{ - api::high::{to_bytes_in, HighSerializer}, - rancor, - ser::allocator::ArenaHandle, - to_bytes, - util::AlignedVec, + api::high::HighSerializer, rancor, ser::allocator::ArenaHandle, to_bytes, util::AlignedVec, Archive, Deserialize, Serialize, }; diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 242f7f20..bcd9a9ed 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -8,7 +8,7 @@ pub mod musig2_session_mgr; use std::{io, marker::Sync, net::SocketAddr, sync::Arc}; -use bitcoin::{hashes::Hash, secp256k1::PublicKey, Txid, XOnlyPublicKey}; +use bitcoin::{hashes::Hash, Txid, XOnlyPublicKey}; use musig2::{errors::RoundFinalizeError, PartialSignature, PubNonce}; use musig2_session_mgr::Musig2SessionManager; pub use quinn::rustls; @@ -20,7 +20,6 @@ use quinn::{ use rkyv::{ deserialize, rancor::{self, Error}, - to_bytes, util::AlignedVec, }; use secret_service_proto::{ diff --git a/crates/secret-service/src/disk/mod.rs b/crates/secret-service/src/disk/mod.rs index 00a2bcf9..4f8c4873 100644 --- a/crates/secret-service/src/disk/mod.rs +++ b/crates/secret-service/src/disk/mod.rs @@ -62,6 +62,7 @@ impl Service { Ok(Self::new_with_seed(seed)) } + /// Deterministically creates a new service using a given seed pub fn new_with_seed(seed: [u8; 32]) -> Self { let keys = OperatorKeys::new(&Xpriv::new_master(NETWORK, &seed).expect("valid xpriv")) .expect("valid keychain"); diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index 3429bbb9..01814ca0 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -12,7 +12,7 @@ use hkdf::Hkdf; use make_buf::make_buf; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::{PublicKey, SecretKey, SECP256K1}, + secp256k1::{SecretKey, SECP256K1}, FirstRound, KeyAggContext, LiftedSignature, SecNonceSpices, SecondRound, }; use secret_service_proto::v1::traits::{ @@ -131,7 +131,7 @@ impl Musig2Signer for Ms2Signer { } /// First round of the MuSig2 protocol for the server. -#[allow(clippy::missing_debug_implementations)] +#[allow(missing_debug_implementations)] pub struct ServerFirstRound { /// The first round of the MuSig2 protocol. first_round: FirstRound, @@ -200,7 +200,7 @@ impl Musig2SignerFirstRound for ServerFirstRound { } /// Second round of the MuSig2 protocol for the server. -#[allow(clippy::missing_debug_implementations)] +#[allow(missing_debug_implementations)] pub struct ServerSecondRound { /// The second round of the MuSig2 protocol. second_round: SecondRound<[u8; 32]>, diff --git a/crates/secret-service/src/disk/operator.rs b/crates/secret-service/src/disk/operator.rs index dfd2aea5..fc7158c7 100644 --- a/crates/secret-service/src/disk/operator.rs +++ b/crates/secret-service/src/disk/operator.rs @@ -3,7 +3,7 @@ use std::future::Future; use bitcoin::{key::Keypair, XOnlyPublicKey}; -use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; +use musig2::secp256k1::{schnorr::Signature, Message, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{OperatorSigner, Origin, Server}; /// Secret data for the operator. diff --git a/crates/secret-service/src/disk/p2p.rs b/crates/secret-service/src/disk/p2p.rs index c66fb9ed..afcbd8c1 100644 --- a/crates/secret-service/src/disk/p2p.rs +++ b/crates/secret-service/src/disk/p2p.rs @@ -3,7 +3,7 @@ use std::future::Future; use bitcoin::{key::Keypair, XOnlyPublicKey}; -use musig2::secp256k1::{schnorr::Signature, Message, PublicKey, SecretKey, SECP256K1}; +use musig2::secp256k1::{schnorr::Signature, Message, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{P2PSigner, Server}; /// Secret data for the P2P signer. diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs index 80a1bdb7..525e5e2b 100644 --- a/crates/secret-service/src/tests.rs +++ b/crates/secret-service/src/tests.rs @@ -4,7 +4,7 @@ use std::{ time::Duration, }; -use bitcoin::{key::Secp256k1, XOnlyPublicKey}; +use bitcoin::key::Secp256k1; use musig2::secp256k1::Message; use rand::{thread_rng, Rng}; use secret_service_client::SecretServiceClient; diff --git a/crates/secret-service/src/tls.rs b/crates/secret-service/src/tls.rs index 57d3d88d..0b836d3e 100644 --- a/crates/secret-service/src/tls.rs +++ b/crates/secret-service/src/tls.rs @@ -13,7 +13,7 @@ use tracing::{error, info, warn}; use crate::{config::TlsConfig, DEV_MODE}; /// Loads a TLS configuration for the Secret Service server. -pub async fn load_tls(conf: TlsConfig) -> ServerConfig { +pub(crate) async fn load_tls(conf: TlsConfig) -> ServerConfig { let (certs, key) = if let (Some(crt_path), Some(key_path)) = (conf.cert, conf.key) { let key = fs::read(&key_path).await.expect("readable key"); let key = if key_path.extension().is_some_and(|x| x == "der") { From 246b4a8982aacf6b7333b4b4bb07a50a6160a4ba Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 24 Feb 2025 13:07:35 +0000 Subject: [PATCH 24/30] clippy stuff, box wrong responses from server --- crates/secret-service-client/src/lib.rs | 1 + crates/secret-service-client/src/musig2.rs | 28 +++++++++---------- crates/secret-service-client/src/operator.rs | 8 ++---- crates/secret-service-client/src/p2p.rs | 8 ++---- .../secret-service-client/src/stakechain.rs | 2 +- crates/secret-service-client/src/wots.rs | 4 +-- crates/secret-service-proto/src/lib.rs | 2 +- crates/secret-service-proto/src/v1/traits.rs | 2 +- crates/secret-service-proto/src/v1/wire.rs | 1 + crates/secret-service-server/src/bool_arr.rs | 2 +- crates/secret-service-server/src/lib.rs | 2 +- crates/secret-service/src/disk/musig2.rs | 2 +- crates/secret-service/src/main.rs | 2 +- crates/secret-service/src/tests.rs | 2 +- 14 files changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/secret-service-client/src/lib.rs b/crates/secret-service-client/src/lib.rs index 37e3f993..4d41d6c7 100644 --- a/crates/secret-service-client/src/lib.rs +++ b/crates/secret-service-client/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::manual_async_fn)] //! The client crate for the secret service. Provides implementations of the traits that use a QUIC //! connection and wire protocol defined in the [`secret_service_proto`] crate to connect with a //! remote secret service. diff --git a/crates/secret-service-client/src/musig2.rs b/crates/secret-service-client/src/musig2.rs index 1f749f85..bfc69d0b 100644 --- a/crates/secret-service-client/src/musig2.rs +++ b/crates/secret-service-client/src/musig2.rs @@ -54,7 +54,7 @@ impl Musig2Signer for Musig2Client { }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2NewSession(maybe_session_id) = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(match maybe_session_id { @@ -73,10 +73,10 @@ impl Musig2Signer for Musig2Client { let msg = ClientMessage::Musig2Pubkey; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::Musig2Pubkey { pubkey } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; - XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res)) + XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res.into())) } } } @@ -102,7 +102,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; PubNonce::from_bytes(&our_nonce).map_err(|_| ClientError::BadData) } @@ -117,7 +117,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; pubkeys .into_iter() @@ -134,7 +134,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(complete) } @@ -154,7 +154,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(maybe_err.map_or(Ok(()), Err)) } @@ -173,7 +173,7 @@ impl Musig2SignerFirstRound for Musig2FirstRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(match maybe_err { Some(e) => Err(e), @@ -208,7 +208,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; AggNonce::from_bytes(&nonce).map_err(|_| ClientError::BadData) } @@ -223,7 +223,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; pubkeys .into_iter() @@ -242,7 +242,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; musig2::PartialSignature::from_slice(&sig).map_err(|_| ClientError::BadData) } @@ -255,7 +255,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(complete) } @@ -275,7 +275,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(maybe_err.map_or(Ok(()), Err)) } @@ -292,7 +292,7 @@ impl Musig2SignerSecondRound for Musig2SecondRound { }; let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; let ServerMessage::Musig2SecondRoundFinalize(res) = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; let res: Result<_, _> = res.into(); Ok(match res { diff --git a/crates/secret-service-client/src/operator.rs b/crates/secret-service-client/src/operator.rs index 64d1c5f1..7598a3ec 100644 --- a/crates/secret-service-client/src/operator.rs +++ b/crates/secret-service-client/src/operator.rs @@ -35,15 +35,13 @@ impl OperatorSigner for OperatorClient { digest: &[u8; 32], ) -> impl Future::Container> + Send { async move { - let msg = ClientMessage::OperatorSign { - digest: digest.clone(), - }; + let msg = ClientMessage::OperatorSign { digest: *digest }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; match res { ServerMessage::OperatorSign { sig } => { Signature::from_slice(&sig).map_err(|_| ClientError::BadData) } - _ => Err(ClientError::WrongMessage(res)), + _ => Err(ClientError::WrongMessage(res.into())), } } } @@ -56,7 +54,7 @@ impl OperatorSigner for OperatorClient { ServerMessage::OperatorPubkey { pubkey } => { XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) } - _ => Err(ClientError::WrongMessage(res)), + _ => Err(ClientError::WrongMessage(res.into())), } } } diff --git a/crates/secret-service-client/src/p2p.rs b/crates/secret-service-client/src/p2p.rs index d17191a9..ccebb775 100644 --- a/crates/secret-service-client/src/p2p.rs +++ b/crates/secret-service-client/src/p2p.rs @@ -35,12 +35,10 @@ impl P2PSigner for P2PClient { digest: &[u8; 32], ) -> impl Future::Container> + Send { async move { - let msg = ClientMessage::P2PSign { - digest: digest.clone(), - }; + let msg = ClientMessage::P2PSign { digest: *digest }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::P2PSign { sig } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Signature::from_slice(&sig).map_err(|_| ClientError::BadData) } @@ -51,7 +49,7 @@ impl P2PSigner for P2PClient { let msg = ClientMessage::P2PPubkey; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::P2PPubkey { pubkey } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) } diff --git a/crates/secret-service-client/src/stakechain.rs b/crates/secret-service-client/src/stakechain.rs index e679ea81..4bdb6ccd 100644 --- a/crates/secret-service-client/src/stakechain.rs +++ b/crates/secret-service-client/src/stakechain.rs @@ -44,7 +44,7 @@ impl StakeChainPreimages for StakeChainPreimgClient { }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::StakeChainGetPreimage { preimg } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(preimg) } diff --git a/crates/secret-service-client/src/wots.rs b/crates/secret-service-client/src/wots.rs index a678863a..863dbd8a 100644 --- a/crates/secret-service-client/src/wots.rs +++ b/crates/secret-service-client/src/wots.rs @@ -42,7 +42,7 @@ impl WotsSigner for WotsClient { }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::WotsGet160Key { key } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(key) } @@ -62,7 +62,7 @@ impl WotsSigner for WotsClient { }; let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; let ServerMessage::WotsGet256Key { key } = res else { - return Err(ClientError::WrongMessage(res)); + return Err(ClientError::WrongMessage(res.into())); }; Ok(key) } diff --git a/crates/secret-service-proto/src/lib.rs b/crates/secret-service-proto/src/lib.rs index 910fc6b4..bc6c75e8 100644 --- a/crates/secret-service-proto/src/lib.rs +++ b/crates/secret-service-proto/src/lib.rs @@ -1,5 +1,5 @@ //! Protocol definitions for the Secret Service. -#[allow(missing_docs)] // because lints wouldn't shut up with rkyv's Archive proc macro +#[allow(missing_docs)] // because lints wouldn't shut up about rkyv's Archive proc macro pub mod v1; pub mod wire; diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index a8b81d8e..85df25fa 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -315,7 +315,7 @@ pub enum ClientError { Timeout, /// The server sent a message that was not expected. - WrongMessage(ServerMessage), + WrongMessage(Box), /// The server sent a message with an unexpected protocol version. WrongVersion, diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 21ceb3d9..11dab791 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -13,6 +13,7 @@ use super::traits::{Musig2SessionId, SignerIdxOutOfBounds}; /// Various messages the server can send to the client. #[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[allow(clippy::large_enum_variant)] pub enum ServerMessage { /// The message the client sent was invalid. InvalidClientMessage, diff --git a/crates/secret-service-server/src/bool_arr.rs b/crates/secret-service-server/src/bool_arr.rs index 0d310d17..1604b8d5 100644 --- a/crates/secret-service-server/src/bool_arr.rs +++ b/crates/secret-service-server/src/bool_arr.rs @@ -91,7 +91,7 @@ where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, >::Error: Debug; - impl<'a, const N: usize, T> fmt::Debug for DebugValues<'a, N, T> + impl fmt::Debug for DebugValues<'_, N, T> where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + fmt::Debug, >::Error: fmt::Debug, diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index bcd9a9ed..3ec75ba4 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -235,7 +235,7 @@ where break 'block ServerMessage::InvalidClientMessage; }; let Ok(pubkeys) = pubkeys - .into_iter() + .iter() .map(|data| XOnlyPublicKey::from_slice(data)) .collect::, _>>() else { diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index 01814ca0..fb96369e 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -222,7 +222,7 @@ impl Musig2SignerSecondRound for ServerSecondRound { async move { self.second_round .holdouts() - .into_iter() + .iter() .map(|idx| self.ordered_public_keys[*idx]) .collect() } diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 0471009f..3c7887fb 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,7 +1,7 @@ //! Runs the Secret Service. +#![allow(clippy::manual_async_fn)] // use secret_service_server::rustls::ServerConfig; - pub mod config; pub mod disk; #[cfg(test)] diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs index 525e5e2b..317b3258 100644 --- a/crates/secret-service/src/tests.rs +++ b/crates/secret-service/src/tests.rs @@ -33,7 +33,7 @@ async fn e2e() { .with_single_cert(vec![cert], key.into()) .expect("valid config"); let config = secret_service_server::Config { - addr: server_addr.clone(), + addr: server_addr, tls_config: server_tls_config, connection_limit: None, }; From 56ecf98d1d57417b00d5f245e0aace11203e1871 Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 24 Feb 2025 16:19:46 +0000 Subject: [PATCH 25/30] p2p signer just returns the derived secret key --- crates/secret-service-client/src/p2p.rs | 35 +++++--------------- crates/secret-service-proto/src/v1/traits.rs | 9 ++--- crates/secret-service-proto/src/v1/wire.rs | 24 ++++---------- crates/secret-service-server/src/lib.rs | 15 +++------ crates/secret-service/src/disk/p2p.rs | 22 ++++-------- crates/secret-service/src/main.rs | 1 - crates/secret-service/src/tests.rs | 7 +--- 7 files changed, 30 insertions(+), 83 deletions(-) diff --git a/crates/secret-service-client/src/p2p.rs b/crates/secret-service-client/src/p2p.rs index ccebb775..cf552235 100644 --- a/crates/secret-service-client/src/p2p.rs +++ b/crates/secret-service-client/src/p2p.rs @@ -1,9 +1,8 @@ //! P2P signer client -use std::{future::Future, sync::Arc}; +use std::sync::Arc; -use bitcoin::XOnlyPublicKey; -use musig2::secp256k1::schnorr::Signature; +use musig2::secp256k1::SecretKey; use quinn::Connection; use secret_service_proto::v1::{ traits::{Client, ClientError, Origin, P2PSigner}, @@ -30,28 +29,12 @@ impl P2PClient { } impl P2PSigner for P2PClient { - fn sign( - &self, - digest: &[u8; 32], - ) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::P2PSign { digest: *digest }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::P2PSign { sig } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Signature::from_slice(&sig).map_err(|_| ClientError::BadData) - } - } - - fn pubkey(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::P2PPubkey; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::P2PPubkey { pubkey } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) - } + async fn secret_key(&self) -> ::Container { + let msg = ClientMessage::P2PSecretKey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::P2PSecretKey { key } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(SecretKey::from_slice(&key).expect("correct length")) } } diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 85df25fa..8dca4c50 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -5,7 +5,7 @@ use std::future::Future; use bitcoin::{Txid, XOnlyPublicKey}; use musig2::{ errors::{RoundContributionError, RoundFinalizeError}, - secp256k1::schnorr::Signature, + secp256k1::{schnorr::Signature, SecretKey}, AggNonce, LiftedSignature, PartialSignature, PubNonce, }; use quinn::{ConnectionError, ReadExactError, WriteError}; @@ -77,11 +77,8 @@ pub trait OperatorSigner: Send { /// The user should make sure the operator's secret key should have its own unique key that isn't /// used for any other purpose. pub trait P2PSigner: Send { - /// Signs a `digest` using the operator's [`SecretKey`](bitcoin::secp256k1::SecretKey). - fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; - - /// Returns the public key of the operator's secret key. - fn pubkey(&self) -> impl Future> + Send; + /// Returns the [`SecretKey`] that should be used for signing P2P messages + fn secret_key(&self) -> impl Future> + Send; } /// Uniquely identifies an in-memory MuSig2 session on the signing server. diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 11dab791..33f3aa44 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -36,16 +36,10 @@ pub enum ServerMessage { pubkey: [u8; 32], }, - /// Response for [`P2PSigner::sign`](super::traits::P2PSigner::sign). - P2PSign { - /// Schnorr signature of for a certain message. - sig: [u8; 64], - }, - - /// Response for [`P2PSigner::pubkey`](super::traits::P2PSigner::pubkey). - P2PPubkey { - /// Serialized Schnorr [`XOnlyPublicKey`] for P2P signatures. - pubkey: [u8; 32], + /// Response for [`P2PSigner::secret_key`](super::traits::P2PSigner::secret_key). + P2PSecretKey { + /// Serialized [`SecretKey`](musig::secp256k1::SecretKey) + key: [u8; 32], }, /// Response for [`Musig2Signer::new_session`](super::traits::Musig2Signer::new_session). @@ -197,14 +191,8 @@ pub enum ClientMessage { /// Request for [`OperatorSigner::pubkey`](super::traits::OperatorSigner::pubkey). OperatorPubkey, - /// Request for [`P2PSigner::sign`](super::traits::P2PSigner::sign). - P2PSign { - /// The digest of the data the client wants signed. - digest: [u8; 32], - }, - - /// Request for [`P2PSigner::pubkey`](super::traits::P2PSigner::pubkey). - P2PPubkey, + /// Request for [`P2PSigner::secret_key`](super::traits::P2PSigner::secret_key). + P2PSecretKey, /// Request for [`Musig2Signer::new_session`](super::traits::Musig2Signer::new_session). Musig2NewSession { diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 3ec75ba4..57c8ef88 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -205,17 +205,10 @@ where } } - ArchivedClientMessage::P2PSign { digest } => { - let sig = service.p2p_signer().sign(digest).await; - ServerMessage::P2PSign { - sig: sig.serialize(), - } - } - - ArchivedClientMessage::P2PPubkey => { - let pubkey = service.p2p_signer().pubkey().await; - ServerMessage::P2PPubkey { - pubkey: pubkey.serialize(), + ArchivedClientMessage::P2PSecretKey => { + let key = service.p2p_signer().secret_key().await; + ServerMessage::P2PSecretKey { + key: key.secret_bytes(), } } diff --git a/crates/secret-service/src/disk/p2p.rs b/crates/secret-service/src/disk/p2p.rs index afcbd8c1..320eaa29 100644 --- a/crates/secret-service/src/disk/p2p.rs +++ b/crates/secret-service/src/disk/p2p.rs @@ -1,32 +1,24 @@ //! In-memory persistence for operator's P2P secret data. -use std::future::Future; - -use bitcoin::{key::Keypair, XOnlyPublicKey}; -use musig2::secp256k1::{schnorr::Signature, Message, SecretKey, SECP256K1}; -use secret_service_proto::v1::traits::{P2PSigner, Server}; +use musig2::secp256k1::SecretKey; +use secret_service_proto::v1::traits::{Origin, P2PSigner, Server}; /// Secret data for the P2P signer. #[derive(Debug)] pub struct ServerP2PSigner { - /// The [`Keypair`] for the P2P signer. - kp: Keypair, + /// The [`SecretKey`] for the P2P signer. + sk: SecretKey, } impl ServerP2PSigner { /// Creates a new [`ServerP2PSigner`] with the given secret key. pub fn new(sk: SecretKey) -> Self { - let kp = Keypair::from_secret_key(SECP256K1, &sk); - Self { kp } + Self { sk } } } impl P2PSigner for ServerP2PSigner { - fn sign(&self, digest: &[u8; 32]) -> impl Future + Send { - async move { self.kp.sign_schnorr(Message::from_digest(*digest)) } - } - - fn pubkey(&self) -> impl Future + Send { - async move { self.kp.x_only_public_key().0 } + async fn secret_key(&self) -> ::Container { + self.sk } } diff --git a/crates/secret-service/src/main.rs b/crates/secret-service/src/main.rs index 3c7887fb..9c5a975b 100644 --- a/crates/secret-service/src/main.rs +++ b/crates/secret-service/src/main.rs @@ -1,5 +1,4 @@ //! Runs the Secret Service. -#![allow(clippy::manual_async_fn)] // use secret_service_server::rustls::ServerConfig; pub mod config; diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs index 317b3258..43103019 100644 --- a/crates/secret-service/src/tests.rs +++ b/crates/secret-service/src/tests.rs @@ -73,12 +73,7 @@ async fn e2e() { // p2p signer let p2p_signer = client.p2p_signer(); - let pubkey = p2p_signer.pubkey().await.expect("good response"); - let to_sign = rng.gen(); - let sig = p2p_signer.sign(&to_sign).await.expect("good response"); - assert!(secp_ctx - .verify_schnorr(&sig, &Message::from_digest(to_sign), &pubkey) - .is_ok()); + p2p_signer.secret_key().await.expect("good response"); } /// Dummy certificate verifier that treats any certificate as valid. From b0803525860320d699a65e4ad9c1129b813b0374 Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 24 Feb 2025 16:27:10 +0000 Subject: [PATCH 26/30] async traits! yay --- crates/secret-service-client/src/musig2.rs | 354 ++++++++---------- crates/secret-service-client/src/operator.rs | 37 +- .../secret-service-client/src/stakechain.rs | 28 +- crates/secret-service-client/src/wots.rs | 54 ++- crates/secret-service/src/disk/musig2.rs | 228 +++++------ crates/secret-service/src/disk/operator.rs | 17 +- crates/secret-service/src/disk/stakechain.rs | 28 +- crates/secret-service/src/disk/wots.rs | 56 +-- 8 files changed, 346 insertions(+), 456 deletions(-) diff --git a/crates/secret-service-client/src/musig2.rs b/crates/secret-service-client/src/musig2.rs index bfc69d0b..e68974e4 100644 --- a/crates/secret-service-client/src/musig2.rs +++ b/crates/secret-service-client/src/musig2.rs @@ -1,6 +1,6 @@ //! MuSig2 signer client -use std::{future::Future, sync::Arc}; +use std::sync::Arc; use bitcoin::{hashes::Hash, Txid, XOnlyPublicKey}; use musig2::{ @@ -37,47 +37,42 @@ impl Musig2Client { } impl Musig2Signer for Musig2Client { - fn new_session( + async fn new_session( &self, pubkeys: Vec, witness: TaprootWitness, input_txid: Txid, input_vout: u32, - ) -> impl Future, ClientError>> + Send - { - async move { - let msg = ClientMessage::Musig2NewSession { - pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), - witness: witness.into(), - input_txid: input_txid.to_byte_array(), - input_vout, - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::Musig2NewSession(maybe_session_id) = res else { - return Err(ClientError::WrongMessage(res.into())); - }; + ) -> Result, ClientError> { + let msg = ClientMessage::Musig2NewSession { + pubkeys: pubkeys.into_iter().map(|pk| pk.serialize()).collect(), + witness: witness.into(), + input_txid: input_txid.to_byte_array(), + input_vout, + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2NewSession(maybe_session_id) = res else { + return Err(ClientError::WrongMessage(res.into())); + }; - Ok(match maybe_session_id { - Ok(session_id) => Ok(Musig2FirstRound { - session_id, - connection: self.conn.clone(), - config: self.config.clone(), - }), - Err(e) => Err(e), - }) - } + Ok(match maybe_session_id { + Ok(session_id) => Ok(Musig2FirstRound { + session_id, + connection: self.conn.clone(), + config: self.config.clone(), + }), + Err(e) => Err(e), + }) } - fn pubkey(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2Pubkey; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::Musig2Pubkey { pubkey } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; + async fn pubkey(&self) -> ::Container { + let msg = ClientMessage::Musig2Pubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::Musig2Pubkey { pubkey } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; - XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res.into())) - } + XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::WrongMessage(res.into())) } } @@ -95,95 +90,80 @@ pub struct Musig2FirstRound { } impl Musig2SignerFirstRound for Musig2FirstRound { - fn our_nonce(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundOurNonce { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - PubNonce::from_bytes(&our_nonce).map_err(|_| ClientError::BadData) - } + async fn our_nonce(&self) -> ::Container { + let msg = ClientMessage::Musig2FirstRoundOurNonce { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundOurNonce { our_nonce } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + PubNonce::from_bytes(&our_nonce).map_err(|_| ClientError::BadData) } - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundHoldouts { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - pubkeys - .into_iter() - .map(|pk| XOnlyPublicKey::from_slice(&pk)) - .collect::, musig2::secp256k1::Error>>() - .map_err(|_| ClientError::BadData) - } + async fn holdouts(&self) -> ::Container> { + let msg = ClientMessage::Musig2FirstRoundHoldouts { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundHoldouts { pubkeys } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + pubkeys + .into_iter() + .map(|pk| XOnlyPublicKey::from_slice(&pk)) + .collect::, musig2::secp256k1::Error>>() + .map_err(|_| ClientError::BadData) } - fn is_complete(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundIsComplete { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(complete) - } + async fn is_complete(&self) -> ::Container { + let msg = ClientMessage::Musig2FirstRoundIsComplete { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundIsComplete { complete } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(complete) } - fn receive_pub_nonce( + async fn receive_pub_nonce( &mut self, pubkey: XOnlyPublicKey, pubnonce: PubNonce, - ) -> impl Future::Container>> + Send - { - async move { - let msg = ClientMessage::Musig2FirstRoundReceivePubNonce { - session_id: self.session_id, - pubkey: pubkey.serialize(), - pubnonce: pubnonce.serialize(), - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(maybe_err.map_or(Ok(()), Err)) - } + ) -> ::Container> { + let msg = ClientMessage::Musig2FirstRoundReceivePubNonce { + session_id: self.session_id, + pubkey: pubkey.serialize(), + pubnonce: pubnonce.serialize(), + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundReceivePubNonce(maybe_err) = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(maybe_err.map_or(Ok(()), Err)) } - fn finalize( + async fn finalize( self, hash: [u8; 32], - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { - let msg = ClientMessage::Musig2FirstRoundFinalize { + ) -> ::Container> { + let msg = ClientMessage::Musig2FirstRoundFinalize { + session_id: self.session_id, + digest: hash, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(match maybe_err { + Some(e) => Err(e), + None => Ok(Musig2SecondRound { session_id: self.session_id, - digest: hash, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2FirstRoundFinalize(maybe_err) = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(match maybe_err { - Some(e) => Err(e), - None => Ok(Musig2SecondRound { - session_id: self.session_id, - connection: self.connection, - config: self.config, - }), - }) - } + connection: self.connection, + config: self.config, + }), + }) } } @@ -201,108 +181,88 @@ pub struct Musig2SecondRound { } impl Musig2SignerSecondRound for Musig2SecondRound { - fn agg_nonce(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundAggNonce { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - AggNonce::from_bytes(&nonce).map_err(|_| ClientError::BadData) - } + async fn agg_nonce(&self) -> ::Container { + let msg = ClientMessage::Musig2SecondRoundAggNonce { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundAggNonce { nonce } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + AggNonce::from_bytes(&nonce).map_err(|_| ClientError::BadData) } - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundHoldouts { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - pubkeys - .into_iter() - .map(|pk| XOnlyPublicKey::from_slice(&pk)) - .collect::, musig2::secp256k1::Error>>() - .map_err(|_| ClientError::BadData) - } + async fn holdouts(&self) -> ::Container> { + let msg = ClientMessage::Musig2SecondRoundHoldouts { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundHoldouts { pubkeys } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + pubkeys + .into_iter() + .map(|pk| XOnlyPublicKey::from_slice(&pk)) + .collect::, musig2::secp256k1::Error>>() + .map_err(|_| ClientError::BadData) } - fn our_signature( - &self, - ) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundOurSignature { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - musig2::PartialSignature::from_slice(&sig).map_err(|_| ClientError::BadData) - } + async fn our_signature(&self) -> ::Container { + let msg = ClientMessage::Musig2SecondRoundOurSignature { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundOurSignature { sig } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + musig2::PartialSignature::from_slice(&sig).map_err(|_| ClientError::BadData) } - fn is_complete(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundIsComplete { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(complete) - } + async fn is_complete(&self) -> ::Container { + let msg = ClientMessage::Musig2SecondRoundIsComplete { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundIsComplete { complete } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(complete) } - fn receive_signature( + async fn receive_signature( &mut self, pubkey: XOnlyPublicKey, signature: musig2::PartialSignature, - ) -> impl Future::Container>> + Send - { - async move { - let msg = ClientMessage::Musig2SecondRoundReceiveSignature { - session_id: self.session_id, - pubkey: pubkey.serialize(), - signature: signature.serialize(), - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(maybe_err.map_or(Ok(()), Err)) - } + ) -> ::Container> { + let msg = ClientMessage::Musig2SecondRoundReceiveSignature { + session_id: self.session_id, + pubkey: pubkey.serialize(), + signature: signature.serialize(), + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundReceiveSignature(maybe_err) = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(maybe_err.map_or(Ok(()), Err)) } - fn finalize( + async fn finalize( self, - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { - let msg = ClientMessage::Musig2SecondRoundFinalize { - session_id: self.session_id, - }; - let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; - let ServerMessage::Musig2SecondRoundFinalize(res) = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - let res: Result<_, _> = res.into(); - Ok(match res { - Ok(sig) => { - let sig = - LiftedSignature::from_bytes(&sig).map_err(|_| ClientError::BadData)?; - Ok(sig) - } - Err(e) => Err(e), - }) - } + ) -> ::Container> { + let msg = ClientMessage::Musig2SecondRoundFinalize { + session_id: self.session_id, + }; + let res = make_v1_req(&self.connection, msg, self.config.timeout).await?; + let ServerMessage::Musig2SecondRoundFinalize(res) = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + let res: Result<_, _> = res.into(); + Ok(match res { + Ok(sig) => { + let sig = LiftedSignature::from_bytes(&sig).map_err(|_| ClientError::BadData)?; + Ok(sig) + } + Err(e) => Err(e), + }) } } diff --git a/crates/secret-service-client/src/operator.rs b/crates/secret-service-client/src/operator.rs index 7598a3ec..6b717a17 100644 --- a/crates/secret-service-client/src/operator.rs +++ b/crates/secret-service-client/src/operator.rs @@ -1,6 +1,6 @@ //! Operator signer client -use std::{future::Future, sync::Arc}; +use std::sync::Arc; use bitcoin::XOnlyPublicKey; use musig2::secp256k1::schnorr::Signature; @@ -30,32 +30,25 @@ impl OperatorClient { } impl OperatorSigner for OperatorClient { - fn sign( - &self, - digest: &[u8; 32], - ) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::OperatorSign { digest: *digest }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - match res { - ServerMessage::OperatorSign { sig } => { - Signature::from_slice(&sig).map_err(|_| ClientError::BadData) - } - _ => Err(ClientError::WrongMessage(res.into())), + async fn sign(&self, digest: &[u8; 32]) -> ::Container { + let msg = ClientMessage::OperatorSign { digest: *digest }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + match res { + ServerMessage::OperatorSign { sig } => { + Signature::from_slice(&sig).map_err(|_| ClientError::BadData) } + _ => Err(ClientError::WrongMessage(res.into())), } } - fn pubkey(&self) -> impl Future::Container> + Send { - async move { - let msg = ClientMessage::OperatorPubkey; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - match res { - ServerMessage::OperatorPubkey { pubkey } => { - XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) - } - _ => Err(ClientError::WrongMessage(res.into())), + async fn pubkey(&self) -> ::Container { + let msg = ClientMessage::OperatorPubkey; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + match res { + ServerMessage::OperatorPubkey { pubkey } => { + XOnlyPublicKey::from_slice(&pubkey).map_err(|_| ClientError::BadData) } + _ => Err(ClientError::WrongMessage(res.into())), } } } diff --git a/crates/secret-service-client/src/stakechain.rs b/crates/secret-service-client/src/stakechain.rs index 4bdb6ccd..381c687f 100644 --- a/crates/secret-service-client/src/stakechain.rs +++ b/crates/secret-service-client/src/stakechain.rs @@ -1,6 +1,6 @@ //! Stake Chain preimages client -use std::{future::Future, sync::Arc}; +use std::sync::Arc; use bitcoin::{hashes::Hash, Txid}; use quinn::Connection; @@ -30,23 +30,21 @@ impl StakeChainPreimgClient { } impl StakeChainPreimages for StakeChainPreimgClient { - fn get_preimg( + async fn get_preimg( &self, prestake_txid: Txid, prestake_vout: u32, stake_index: u32, - ) -> impl Future::Container<[u8; 32]>> + Send { - async move { - let msg = ClientMessage::StakeChainGetPreimage { - prestake_txid: prestake_txid.to_byte_array(), - prestake_vout, - stake_index, - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::StakeChainGetPreimage { preimg } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(preimg) - } + ) -> ::Container<[u8; 32]> { + let msg = ClientMessage::StakeChainGetPreimage { + prestake_txid: prestake_txid.to_byte_array(), + prestake_vout, + stake_index, + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::StakeChainGetPreimage { preimg } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(preimg) } } diff --git a/crates/secret-service-client/src/wots.rs b/crates/secret-service-client/src/wots.rs index 863dbd8a..7c5d649f 100644 --- a/crates/secret-service-client/src/wots.rs +++ b/crates/secret-service-client/src/wots.rs @@ -1,5 +1,5 @@ //! Winternitz One-time Signature (WOTS) signer client -use std::{future::Future, sync::Arc}; +use std::sync::Arc; use bitcoin::{hashes::Hash, Txid}; use quinn::Connection; @@ -28,43 +28,39 @@ impl WotsClient { } impl WotsSigner for WotsClient { - fn get_160_key( + async fn get_160_key( &self, txid: Txid, vout: u32, index: u32, - ) -> impl Future::Container<[u8; 20 * 160]>> + Send { - async move { - let msg = ClientMessage::WotsGet160Key { - index, - vout, - txid: txid.as_raw_hash().to_byte_array(), - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::WotsGet160Key { key } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(key) - } + ) -> ::Container<[u8; 20 * 160]> { + let msg = ClientMessage::WotsGet160Key { + index, + vout, + txid: txid.as_raw_hash().to_byte_array(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::WotsGet160Key { key } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(key) } - fn get_256_key( + async fn get_256_key( &self, txid: Txid, vout: u32, index: u32, - ) -> impl Future::Container<[u8; 20 * 256]>> + Send { - async move { - let msg = ClientMessage::WotsGet256Key { - index, - vout, - txid: txid.as_raw_hash().to_byte_array(), - }; - let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; - let ServerMessage::WotsGet256Key { key } = res else { - return Err(ClientError::WrongMessage(res.into())); - }; - Ok(key) - } + ) -> ::Container<[u8; 20 * 256]> { + let msg = ClientMessage::WotsGet256Key { + index, + vout, + txid: txid.as_raw_hash().to_byte_array(), + }; + let res = make_v1_req(&self.conn, msg, self.config.timeout).await?; + let ServerMessage::WotsGet256Key { key } = res else { + return Err(ClientError::WrongMessage(res.into())); + }; + Ok(key) } } diff --git a/crates/secret-service/src/disk/musig2.rs b/crates/secret-service/src/disk/musig2.rs index fb96369e..21439d6f 100644 --- a/crates/secret-service/src/disk/musig2.rs +++ b/crates/secret-service/src/disk/musig2.rs @@ -1,7 +1,5 @@ //! In-memory persistence for MuSig2's secret data. -use std::future::Future; - use bitcoin::{ bip32::{ChildNumber, Xpriv}, hashes::Hash, @@ -64,69 +62,67 @@ impl Ms2Signer { } impl Musig2Signer for Ms2Signer { - fn new_session( + async fn new_session( &self, mut pubkeys: Vec, witness: TaprootWitness, input_txid: Txid, input_vout: u32, - ) -> impl Future> + Send { - async move { - let my_pub_key = self.kp.x_only_public_key().0; - if !pubkeys.contains(&my_pub_key) { - pubkeys.push(my_pub_key); + ) -> Result { + let my_pub_key = self.kp.x_only_public_key().0; + if !pubkeys.contains(&my_pub_key) { + pubkeys.push(my_pub_key); + } + pubkeys.sort(); + let signer_index = pubkeys.iter().position(|pk| pk == &my_pub_key).unwrap(); + let mut ctx = + KeyAggContext::new(pubkeys.iter().map(|pk| pk.public_key(Parity::Even))).unwrap(); + + match witness { + TaprootWitness::Key => { + ctx = ctx + .with_unspendable_taproot_tweak() + .expect("must be able to tweak the key agg context") } - pubkeys.sort(); - let signer_index = pubkeys.iter().position(|pk| pk == &my_pub_key).unwrap(); - let mut ctx = - KeyAggContext::new(pubkeys.iter().map(|pk| pk.public_key(Parity::Even))).unwrap(); - - match witness { - TaprootWitness::Key => { - ctx = ctx - .with_unspendable_taproot_tweak() - .expect("must be able to tweak the key agg context") - } - TaprootWitness::Tweaked { tweak } => { - ctx = ctx - .with_taproot_tweak(tweak.as_ref()) - .expect("must be able to tweak the key agg context") - } - _ => {} + TaprootWitness::Tweaked { tweak } => { + ctx = ctx + .with_taproot_tweak(tweak.as_ref()) + .expect("must be able to tweak the key agg context") } + _ => {} + } - let nonce_seed = { - let info = make_buf! { - (&input_txid.as_raw_hash().to_byte_array(), 32), - (&input_vout.to_le_bytes(), 4) - }; - let hk = Hkdf::::new(None, &self.ikm); - let mut okm = [0u8; 32]; - hk.expand(&info, &mut okm) - .expect("32 is a valid length for Sha256 to output"); - okm + let nonce_seed = { + let info = make_buf! { + (&input_txid.as_raw_hash().to_byte_array(), 32), + (&input_vout.to_le_bytes(), 4) }; - - let first_round = FirstRound::new( - ctx, - nonce_seed, - signer_index, - SecNonceSpices::new().with_seckey(self.kp.secret_key()), - ) - .map_err(|e| SignerIdxOutOfBounds { - index: e.index, - n_signers: e.n_signers, - })?; - Ok(ServerFirstRound { - first_round, - ordered_public_keys: pubkeys, - seckey: self.kp.secret_key(), - }) - } + let hk = Hkdf::::new(None, &self.ikm); + let mut okm = [0u8; 32]; + hk.expand(&info, &mut okm) + .expect("32 is a valid length for Sha256 to output"); + okm + }; + + let first_round = FirstRound::new( + ctx, + nonce_seed, + signer_index, + SecNonceSpices::new().with_seckey(self.kp.secret_key()), + ) + .map_err(|e| SignerIdxOutOfBounds { + index: e.index, + n_signers: e.n_signers, + })?; + Ok(ServerFirstRound { + first_round, + ordered_public_keys: pubkeys, + seckey: self.kp.secret_key(), + }) } - fn pubkey(&self) -> impl Future::Container> + Send { - async move { self.kp.x_only_public_key().0 } + async fn pubkey(&self) -> ::Container { + self.kp.x_only_public_key().0 } } @@ -144,58 +140,45 @@ pub struct ServerFirstRound { } impl Musig2SignerFirstRound for ServerFirstRound { - fn our_nonce( - &self, - ) -> impl Future::Container> + Send { - async move { self.first_round.our_public_nonce() } + async fn our_nonce(&self) -> ::Container { + self.first_round.our_public_nonce() } - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - self.first_round - .holdouts() - .iter() - .map(|idx| self.ordered_public_keys[*idx]) - .collect() - } + async fn holdouts(&self) -> ::Container> { + self.first_round + .holdouts() + .iter() + .map(|idx| self.ordered_public_keys[*idx]) + .collect() } - fn is_complete(&self) -> impl Future::Container> + Send { - async move { self.first_round.is_complete() } + async fn is_complete(&self) -> ::Container { + self.first_round.is_complete() } - fn receive_pub_nonce( + async fn receive_pub_nonce( &mut self, pubkey: XOnlyPublicKey, pubnonce: musig2::PubNonce, - ) -> impl Future::Container>> + Send - { - async move { - let signer_idx = self - .ordered_public_keys - .iter() - .position(|x| x == &pubkey) - .ok_or(RoundContributionError::out_of_range(0, 0))?; - self.first_round.receive_nonce(signer_idx, pubnonce) - } + ) -> ::Container> { + let signer_idx = self + .ordered_public_keys + .iter() + .position(|x| x == &pubkey) + .ok_or(RoundContributionError::out_of_range(0, 0))?; + self.first_round.receive_nonce(signer_idx, pubnonce) } - fn finalize( + async fn finalize( self, hash: [u8; 32], - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { - self.first_round - .finalize(self.seckey, hash) - .map(|sr| ServerSecondRound { - second_round: sr, - ordered_public_keys: self.ordered_public_keys, - }) - } + ) -> ::Container> { + self.first_round + .finalize(self.seckey, hash) + .map(|sr| ServerSecondRound { + second_round: sr, + ordered_public_keys: self.ordered_public_keys, + }) } } @@ -210,55 +193,42 @@ pub struct ServerSecondRound { } impl Musig2SignerSecondRound for ServerSecondRound { - fn agg_nonce( - &self, - ) -> impl Future::Container> + Send { - async move { self.second_round.aggregated_nonce().clone() } + async fn agg_nonce(&self) -> ::Container { + self.second_round.aggregated_nonce().clone() } - fn holdouts( - &self, - ) -> impl Future::Container>> + Send { - async move { - self.second_round - .holdouts() - .iter() - .map(|idx| self.ordered_public_keys[*idx]) - .collect() - } + async fn holdouts(&self) -> ::Container> { + self.second_round + .holdouts() + .iter() + .map(|idx| self.ordered_public_keys[*idx]) + .collect() } - fn our_signature( - &self, - ) -> impl Future::Container> + Send { - async move { self.second_round.our_signature() } + async fn our_signature(&self) -> ::Container { + self.second_round.our_signature() } - fn is_complete(&self) -> impl Future::Container> + Send { - async move { self.second_round.is_complete() } + async fn is_complete(&self) -> ::Container { + self.second_round.is_complete() } - fn receive_signature( + async fn receive_signature( &mut self, pubkey: XOnlyPublicKey, signature: musig2::PartialSignature, - ) -> impl Future::Container>> + Send - { - async move { - let signer_idx = self - .ordered_public_keys - .iter() - .position(|x| x == &pubkey) - .ok_or(RoundContributionError::out_of_range(0, 0))?; - self.second_round.receive_signature(signer_idx, signature) - } + ) -> ::Container> { + let signer_idx = self + .ordered_public_keys + .iter() + .position(|x| x == &pubkey) + .ok_or(RoundContributionError::out_of_range(0, 0))?; + self.second_round.receive_signature(signer_idx, signature) } - fn finalize( + async fn finalize( self, - ) -> impl Future< - Output = ::Container>, - > + Send { - async move { self.second_round.finalize() } + ) -> ::Container> { + self.second_round.finalize() } } diff --git a/crates/secret-service/src/disk/operator.rs b/crates/secret-service/src/disk/operator.rs index fc7158c7..5a72e76d 100644 --- a/crates/secret-service/src/disk/operator.rs +++ b/crates/secret-service/src/disk/operator.rs @@ -1,7 +1,5 @@ //! In-memory persistence for operator's secret data. -use std::future::Future; - use bitcoin::{key::Keypair, XOnlyPublicKey}; use musig2::secp256k1::{schnorr::Signature, Message, SecretKey, SECP256K1}; use secret_service_proto::v1::traits::{OperatorSigner, Origin, Server}; @@ -22,17 +20,12 @@ impl Operator { } impl OperatorSigner for Operator { - fn sign( - &self, - digest: &[u8; 32], - ) -> impl Future::Container> + Send { - async move { - self.kp - .sign_schnorr(Message::from_digest_slice(digest).unwrap()) - } + async fn sign(&self, digest: &[u8; 32]) -> ::Container { + self.kp + .sign_schnorr(Message::from_digest_slice(digest).unwrap()) } - fn pubkey(&self) -> impl Future::Container> + Send { - async move { self.kp.x_only_public_key().0 } + async fn pubkey(&self) -> ::Container { + self.kp.x_only_public_key().0 } } diff --git a/crates/secret-service/src/disk/stakechain.rs b/crates/secret-service/src/disk/stakechain.rs index fe621685..e4217d4a 100644 --- a/crates/secret-service/src/disk/stakechain.rs +++ b/crates/secret-service/src/disk/stakechain.rs @@ -1,7 +1,5 @@ //! In-memory persistence for Stake Chain preimages. -use std::future::Future; - use bitcoin::{ bip32::{ChildNumber, Xpriv}, hashes::Hash, @@ -42,23 +40,21 @@ impl StakeChain { impl StakeChainPreimages for StakeChain { /// Gets a preimage for a Stake Chain, given a pre-stake transaction ID, and output index; and /// stake index. - fn get_preimg( + async fn get_preimg( &self, prestake_txid: Txid, prestake_vout: u32, stake_index: u32, - ) -> impl Future + Send { - async move { - let hk = Hkdf::::new(None, &self.ikm); - let mut okm = [0u8; 32]; - let info = make_buf! { - (prestake_txid.as_raw_hash().as_byte_array(), 32), - (&prestake_vout.to_le_bytes(), 4), - (&stake_index.to_le_bytes(), 4) - }; - hk.expand(&info, &mut okm) - .expect("32 is a valid length for Sha256 to output"); - okm - } + ) -> [u8; 32] { + let hk = Hkdf::::new(None, &self.ikm); + let mut okm = [0u8; 32]; + let info = make_buf! { + (prestake_txid.as_raw_hash().as_byte_array(), 32), + (&prestake_vout.to_le_bytes(), 4), + (&stake_index.to_le_bytes(), 4) + }; + hk.expand(&info, &mut okm) + .expect("32 is a valid length for Sha256 to output"); + okm } } diff --git a/crates/secret-service/src/disk/wots.rs b/crates/secret-service/src/disk/wots.rs index e12c2df9..53f3e819 100644 --- a/crates/secret-service/src/disk/wots.rs +++ b/crates/secret-service/src/disk/wots.rs @@ -1,7 +1,5 @@ //! In-memory persistence for the Winternitz One-Time Signature (WOTS) keys. -use std::future::Future; - use bitcoin::{ bip32::{ChildNumber, Xpriv}, hashes::Hash, @@ -57,41 +55,27 @@ impl SeededWotsSigner { } impl WotsSigner for SeededWotsSigner { - fn get_160_key( - &self, - txid: Txid, - vout: u32, - index: u32, - ) -> impl Future + Send { - async move { - let hk = Hkdf::::new(None, &self.ikm_160); - let mut okm = [0u8; 20 * 160]; - let info = make_buf! { - (txid.as_raw_hash().as_byte_array(), 32), - (&vout.to_le_bytes(), 4), - (&index.to_le_bytes(), 4), - }; - hk.expand(&info, &mut okm).expect("valid output length"); - okm - } + async fn get_160_key(&self, txid: Txid, vout: u32, index: u32) -> [u8; 20 * 160] { + let hk = Hkdf::::new(None, &self.ikm_160); + let mut okm = [0u8; 20 * 160]; + let info = make_buf! { + (txid.as_raw_hash().as_byte_array(), 32), + (&vout.to_le_bytes(), 4), + (&index.to_le_bytes(), 4), + }; + hk.expand(&info, &mut okm).expect("valid output length"); + okm } - fn get_256_key( - &self, - txid: Txid, - vout: u32, - index: u32, - ) -> impl Future + Send { - async move { - let hk = Hkdf::::new(None, &self.ikm_256); - let mut okm = [0u8; 20 * 256]; - let info = make_buf! { - (txid.as_raw_hash().as_byte_array(), 32), - (&vout.to_le_bytes(), 4), - (&index.to_le_bytes(), 4), - }; - hk.expand(&info, &mut okm).expect("valid output length"); - okm - } + async fn get_256_key(&self, txid: Txid, vout: u32, index: u32) -> [u8; 20 * 256] { + let hk = Hkdf::::new(None, &self.ikm_256); + let mut okm = [0u8; 20 * 256]; + let info = make_buf! { + (txid.as_raw_hash().as_byte_array(), 32), + (&vout.to_le_bytes(), 4), + (&index.to_le_bytes(), 4), + }; + hk.expand(&info, &mut okm).expect("valid output length"); + okm } } From 33662c8e3332d0260176e9c49a4d902a4e90ee88 Mon Sep 17 00:00:00 2001 From: Azz Date: Mon, 24 Feb 2025 16:44:27 +0000 Subject: [PATCH 27/30] chad signature test --- crates/secret-service/src/tests.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs index 43103019..c6d3851b 100644 --- a/crates/secret-service/src/tests.rs +++ b/crates/secret-service/src/tests.rs @@ -59,17 +59,25 @@ async fn e2e() { .await .expect("good conn"); - let mut rng = thread_rng(); - let secp_ctx = Secp256k1::verification_only(); - // operator signer let op_signer = client.operator_signer(); let pubkey = op_signer.pubkey().await.expect("good response"); - let to_sign = rng.gen(); - let sig = op_signer.sign(&to_sign).await.expect("good response"); - assert!(secp_ctx - .verify_schnorr(&sig, &Message::from_digest(to_sign), &pubkey) - .is_ok()); + let handles = (0..1000) + .map(|_| { + let secp_ctx = Secp256k1::verification_only(); + let op_signer = op_signer.clone(); + tokio::spawn(async move { + let to_sign = thread_rng().gen(); + let sig = op_signer.sign(&to_sign).await.expect("good response"); + assert!(secp_ctx + .verify_schnorr(&sig, &Message::from_digest(to_sign), &pubkey) + .is_ok()); + }) + }) + .collect::>(); + for handle in handles { + handle.await.unwrap(); + } // p2p signer let p2p_signer = client.p2p_signer(); From 02d2c4465925bfecbeeec6f16e6534f55ff6617b Mon Sep 17 00:00:00 2001 From: Rajil Bajracharya Date: Mon, 24 Feb 2025 22:54:14 +0545 Subject: [PATCH 28/30] chore: revert cargo update and pin deps --- Cargo.lock | 202 +++++++++---------- Cargo.toml | 26 +-- bridge-guest-builder/bridge-guest/Cargo.lock | 12 +- 3 files changed, 119 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2030f10..3beef968 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1238,8 +1238,7 @@ dependencies = [ [[package]] name = "ark-std" version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +source = "git+https://github.com/arkworks-rs/std/?rev=db4367e68ff60da31ac759831e38f60171f4e03d#db4367e68ff60da31ac759831e38f60171f4e03d" dependencies = [ "colored 2.2.0", "num-traits", @@ -2201,7 +2200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -2210,7 +2209,7 @@ version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3022,7 +3021,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4438,7 +4437,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if 1.0.0", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -5824,11 +5823,11 @@ dependencies = [ "strata-bridge-proof-primitives", "strata-btcio", "strata-l1tx", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-proofimpl-btc-blockspace", "strata-state", "tokio", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", + "zkaleido", ] [[package]] @@ -5907,7 +5906,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6547,7 +6546,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8017,7 +8016,7 @@ dependencies = [ "strata-bridge-rpc", "strata-btcio", "strata-common", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-rpc-api", "tokio", "tracing", @@ -8050,7 +8049,7 @@ dependencies = [ "strata-bridge-tx-graph", "strata-btcio", "strata-l1tx", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-rpc-api", "strata-rpc-types", "strata-state", @@ -8100,7 +8099,7 @@ dependencies = [ "secp256k1", "serde", "sha2", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "thiserror 2.0.11", ] @@ -8113,7 +8112,7 @@ dependencies = [ "bitcoin", "borsh", "strata-bridge-test-utils", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-proofimpl-btc-blockspace", "strata-state", ] @@ -8129,12 +8128,12 @@ dependencies = [ "strata-common", "strata-crypto", "strata-l1tx", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-proofimpl-btc-blockspace", "strata-state", "thiserror 2.0.11", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", + "zkaleido", "zkaleido-native-adapter", ] @@ -8164,11 +8163,11 @@ dependencies = [ "strata-bridge-guest-builder", "strata-bridge-primitives", "strata-bridge-proof-protocol", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", - "zkaleido-sp1-adapter 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", + "zkaleido", + "zkaleido-sp1-adapter", ] [[package]] @@ -8181,12 +8180,12 @@ dependencies = [ [[package]] name = "strata-bridge-sig-manager" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "musig2", "strata-db", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-storage", "thiserror 2.0.11", "tracing", @@ -8225,12 +8224,12 @@ dependencies = [ [[package]] name = "strata-bridge-tx-builder" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "musig2", "serde", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "thiserror 2.0.11", ] @@ -8264,7 +8263,7 @@ dependencies = [ [[package]] name = "strata-btcio" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "async-trait", @@ -8276,7 +8275,6 @@ dependencies = [ "musig2", "rand", "reqwest", - "secp256k1", "serde", "serde_json", "sha2", @@ -8284,7 +8282,7 @@ dependencies = [ "strata-config", "strata-db", "strata-l1tx", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-rpc-types", "strata-state", "strata-status", @@ -8299,14 +8297,14 @@ dependencies = [ [[package]] name = "strata-chaintsn" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "bitcoin", "rand_chacha", "rand_core", "strata-eectl", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "thiserror 2.0.11", "tracing", @@ -8315,7 +8313,7 @@ dependencies = [ [[package]] name = "strata-common" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "deadpool", @@ -8324,7 +8322,7 @@ dependencies = [ "opentelemetry-otlp", "opentelemetry_sdk", "serde", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "tracing", "tracing-opentelemetry", "tracing-subscriber 0.3.19", @@ -8333,7 +8331,7 @@ dependencies = [ [[package]] name = "strata-config" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "serde", @@ -8342,10 +8340,9 @@ dependencies = [ [[package]] name = "strata-consensus-logic" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", - "async-trait", "bitcoin", "borsh", "futures", @@ -8357,7 +8354,7 @@ dependencies = [ "strata-db", "strata-eectl", "strata-l1tx", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-rpc-types", "strata-state", "strata-status", @@ -8367,25 +8364,25 @@ dependencies = [ "threadpool", "tokio", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido", "zkaleido-risc0-adapter", - "zkaleido-sp1-adapter 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido-sp1-adapter", ] [[package]] name = "strata-crypto" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "secp256k1", "sha2", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", ] [[package]] name = "strata-db" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "arbitrary", @@ -8396,19 +8393,19 @@ dependencies = [ "parking_lot", "serde", "strata-mmr", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "thiserror 2.0.11", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido", ] [[package]] name = "strata-eectl" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "thiserror 2.0.11", ] @@ -8416,11 +8413,11 @@ dependencies = [ [[package]] name = "strata-key-derivation" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "bitcoin", "secp256k1", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata)", "thiserror 2.0.11", "zeroize", ] @@ -8428,7 +8425,7 @@ dependencies = [ [[package]] name = "strata-l1tx" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "arbitrary", @@ -8437,7 +8434,7 @@ dependencies = [ "hex", "musig2", "strata-bridge-tx-builder", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "thiserror 2.0.11", "tracing", @@ -8446,7 +8443,7 @@ dependencies = [ [[package]] name = "strata-mmr" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "arbitrary", "borsh", @@ -8459,7 +8456,33 @@ dependencies = [ [[package]] name = "strata-primitives" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" +dependencies = [ + "anyhow", + "arbitrary", + "bincode", + "bitcoin", + "bitcoin-bosd", + "borsh", + "const-hex", + "digest 0.10.7", + "hex", + "musig2", + "num_enum 0.7.3", + "rand", + "secp256k1", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.11", + "tracing", + "zeroize", +] + +[[package]] +name = "strata-primitives" +version = "0.1.0" +source = "git+https://github.com/alpenlabs/strata#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" dependencies = [ "anyhow", "arbitrary", @@ -8485,7 +8508,7 @@ dependencies = [ [[package]] name = "strata-proofimpl-btc-blockspace" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "borsh", @@ -8494,15 +8517,15 @@ dependencies = [ "sha2", "strata-bridge-tx-builder", "strata-l1tx", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido", ] [[package]] name = "strata-rpc-api" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "hex", @@ -8513,18 +8536,18 @@ dependencies = [ "strata-bridge-tx-builder", "strata-common", "strata-db", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-rpc-types", "strata-sequencer", "strata-state", "thiserror 2.0.11", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido", ] [[package]] name = "strata-rpc-types" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "hex", @@ -8533,7 +8556,7 @@ dependencies = [ "serde_json", "serde_with", "strata-db", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "thiserror 2.0.11", ] @@ -8541,7 +8564,7 @@ dependencies = [ [[package]] name = "strata-sequencer" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "borsh", @@ -8551,7 +8574,7 @@ dependencies = [ "strata-consensus-logic", "strata-db", "strata-eectl", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "strata-status", "strata-storage", @@ -8564,11 +8587,9 @@ dependencies = [ [[package]] name = "strata-state" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ - "anyhow", "arbitrary", - "async-trait", "bitcoin", "borsh", "digest 0.10.7", @@ -8578,17 +8599,17 @@ dependencies = [ "sha2", "strata-bridge-tx-builder", "strata-crypto", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido", ] [[package]] name = "strata-status" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-rpc-types", "strata-state", "thiserror 2.0.11", @@ -8599,7 +8620,7 @@ dependencies = [ [[package]] name = "strata-storage" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "async-trait", @@ -8608,7 +8629,7 @@ dependencies = [ "parking_lot", "paste", "strata-db", - "strata-primitives", + "strata-primitives 0.1.0 (git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c)", "strata-state", "threadpool", "tokio", @@ -8618,7 +8639,7 @@ dependencies = [ [[package]] name = "strata-tasks" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#8f743b59a7b541ba4b3cebe78db086cf5b6179d0" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "futures-util", @@ -8817,7 +8838,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -9728,7 +9749,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -10181,18 +10202,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "zkaleido" -version = "0.1.0" -source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2#737d8e591be90d84ab7639d8bf1bb2ed36f2735f" -dependencies = [ - "arbitrary", - "bincode", - "borsh", - "serde", - "thiserror 1.0.69", -] - [[package]] name = "zkaleido-native-adapter" version = "0.1.0" @@ -10202,18 +10211,22 @@ dependencies = [ "borsh", "serde", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", + "zkaleido", ] [[package]] name = "zkaleido-risc0-adapter" version = "0.1.0" -source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2#737d8e591be90d84ab7639d8bf1bb2ed36f2735f" +source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1#132da748d0789cd5799d5f949d017141926802e7" dependencies = [ + "bincode", + "borsh", + "hex", "risc0-zkvm", "serde", "sha2", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "tracing-subscriber 0.3.19", + "zkaleido", ] [[package]] @@ -10230,17 +10243,7 @@ dependencies = [ "sp1-sdk", "sp1-verifier", "tracing", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc1)", -] - -[[package]] -name = "zkaleido-sp1-adapter" -version = "0.1.0" -source = "git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2#737d8e591be90d84ab7639d8bf1bb2ed36f2735f" -dependencies = [ - "hex", - "sp1-verifier", - "zkaleido 0.1.0 (git+https://github.com/alpenlabs/zkaleido?tag=v0.1.0-alpha-rc2)", + "zkaleido", ] [[package]] @@ -10338,8 +10341,3 @@ dependencies = [ "cc", "pkg-config", ] - -[[patch.unused]] -name = "ark-std" -version = "0.5.0" -source = "git+https://github.com/arkworks-rs/std/#b0cbdeb097b42f61880b3bee19e8dd37258d23a5" diff --git a/Cargo.toml b/Cargo.toml index a7059eb4..3613eb13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,17 +46,17 @@ strata-bridge-test-utils = { path = "crates/test-utils" } strata-bridge-tx-graph = { path = "crates/tx-graph" } # deps from original strata repo -strata-bridge-tx-builder = { git = "https://github.com/alpenlabs/strata.git" } -strata-btcio = { git = "https://github.com/alpenlabs/strata.git" } -strata-common = { git = "https://github.com/alpenlabs/strata.git" } -strata-config = { git = "https://github.com/alpenlabs/strata.git" } -strata-crypto = { git = "https://github.com/alpenlabs/strata.git" } -strata-l1tx = { git = "https://github.com/alpenlabs/strata.git" } -strata-primitives = { git = "https://github.com/alpenlabs/strata.git" } -strata-proofimpl-btc-blockspace = { git = "https://github.com/alpenlabs/strata.git" } -strata-rpc-api = { git = "https://github.com/alpenlabs/strata.git" } -strata-rpc-types = { git = "https://github.com/alpenlabs/strata.git" } -strata-state = { git = "https://github.com/alpenlabs/strata.git" } +strata-bridge-tx-builder = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-btcio = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-common = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-config = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-crypto = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-l1tx = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-primitives = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-proofimpl-btc-blockspace = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-rpc-api = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-rpc-types = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } +strata-state = { git = "https://github.com/alpenlabs/strata.git", rev = "c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" } zkaleido = { git = "https://github.com/alpenlabs/zkaleido", tag = "v0.1.0-alpha-rc1" } zkaleido-native-adapter = { git = "https://github.com/alpenlabs/zkaleido", tag = "v0.1.0-alpha-rc1" } @@ -72,7 +72,7 @@ ark-ff = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-lo ark-groth16 = { git = "https://github.com/arkworks-rs/groth16" } ark-r1cs-std = { git = "https://github.com/arkworks-rs/r1cs-std/" } ark-relations = { git = "https://github.com/arkworks-rs/snark/" } -ark-std = { git = "https://github.com/arkworks-rs/std/" } +ark-std = { git = "https://github.com/arkworks-rs/std/", rev = "db4367e68ff60da31ac759831e38f60171f4e03d" } async-trait = "0.1.81" base64 = "0.22.1" bincode = "1.3.3" @@ -145,4 +145,4 @@ ark-serialize = { git = "https://github.com/chainwayxyz/algebra/", branch = "new ark-bn254 = { git = "https://github.com/chainwayxyz/algebra/", branch = "new-ate-loop" } ark-crypto-primitives = { git = "https://github.com/arkworks-rs/crypto-primitives/" } ark-groth16 = { git = "https://github.com/arkworks-rs/groth16" } -ark-std = { git = "https://github.com/arkworks-rs/std/" } +ark-std = { git = "https://github.com/arkworks-rs/std/", rev = "db4367e68ff60da31ac759831e38f60171f4e03d" } diff --git a/bridge-guest-builder/bridge-guest/Cargo.lock b/bridge-guest-builder/bridge-guest/Cargo.lock index 633761dc..ac5e847e 100644 --- a/bridge-guest-builder/bridge-guest/Cargo.lock +++ b/bridge-guest-builder/bridge-guest/Cargo.lock @@ -1101,7 +1101,7 @@ dependencies = [ [[package]] name = "strata-bridge-tx-builder" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#1e2b41ab625e68dde34458d97f89ebb21fc8c70e" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "musig2", @@ -1113,7 +1113,7 @@ dependencies = [ [[package]] name = "strata-crypto" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#1e2b41ab625e68dde34458d97f89ebb21fc8c70e" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "secp256k1", "sha2", @@ -1123,7 +1123,7 @@ dependencies = [ [[package]] name = "strata-l1tx" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#1e2b41ab625e68dde34458d97f89ebb21fc8c70e" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "arbitrary", @@ -1141,7 +1141,7 @@ dependencies = [ [[package]] name = "strata-primitives" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#1e2b41ab625e68dde34458d97f89ebb21fc8c70e" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "anyhow", "arbitrary", @@ -1167,7 +1167,7 @@ dependencies = [ [[package]] name = "strata-proofimpl-btc-blockspace" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#1e2b41ab625e68dde34458d97f89ebb21fc8c70e" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "bitcoin", "borsh", @@ -1184,7 +1184,7 @@ dependencies = [ [[package]] name = "strata-state" version = "0.1.0" -source = "git+https://github.com/alpenlabs/strata.git#1e2b41ab625e68dde34458d97f89ebb21fc8c70e" +source = "git+https://github.com/alpenlabs/strata.git?rev=c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c#c5a46a9e80ee12c25868a5276dd13b56fe0c4e7c" dependencies = [ "arbitrary", "bitcoin", From 278fb830620a4e7c04d3467e98ae64fb9950d7ec Mon Sep 17 00:00:00 2001 From: Azz Date: Tue, 25 Feb 2025 12:14:08 +0000 Subject: [PATCH 29/30] fix docs? --- crates/secret-service-proto/src/v1/traits.rs | 2 +- crates/secret-service-proto/src/v1/wire.rs | 20 +++++++++++--------- crates/secret-service/src/tests.rs | 1 + 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/secret-service-proto/src/v1/traits.rs b/crates/secret-service-proto/src/v1/traits.rs index 8dca4c50..410cde62 100644 --- a/crates/secret-service-proto/src/v1/traits.rs +++ b/crates/secret-service-proto/src/v1/traits.rs @@ -63,7 +63,7 @@ where /// The user should make sure the operator's secret key should have its own unique key that isn't /// used for any other purpose. pub trait OperatorSigner: Send { - /// Signs a `digest` using the operator's [`SecretKey`](bitcoin::secp256k1::SecretKey). + /// Signs a `digest` using the operator's [`SecretKey`]. fn sign(&self, digest: &[u8; 32]) -> impl Future> + Send; /// Returns the public key of the operator's secret key. diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 33f3aa44..29da98e8 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -32,13 +32,13 @@ pub enum ServerMessage { /// Response for [`OperatorSigner::pubkey`](super::traits::OperatorSigner::pubkey). OperatorPubkey { - /// Serialized Schnorr [`XOnlyPublicKey`] for operator signatures. + /// Serialized Schnorr [`XOnlyPublicKey`](bitcoin::XOnlyPublicKey) for operator signatures. pubkey: [u8; 32], }, /// Response for [`P2PSigner::secret_key`](super::traits::P2PSigner::secret_key). P2PSecretKey { - /// Serialized [`SecretKey`](musig::secp256k1::SecretKey) + /// Serialized [`SecretKey`](musig2::secp256k1::SecretKey) key: [u8; 32], }, @@ -47,7 +47,7 @@ pub enum ServerMessage { /// Response for [`Musig2Signer::pubkey`](super::traits::Musig2Signer::pubkey). Musig2Pubkey { - /// Serialized Schnorr [`XOnlyPublicKey`] for MuSig2 signatures. + /// Serialized Schnorr [`XOnlyPublicKey`](bitcoin::XOnlyPublicKey) for MuSig2 signatures. pubkey: [u8; 32], }, @@ -61,8 +61,8 @@ pub enum ServerMessage { /// Response for /// [`Musig2SignerFirstRound::holdouts`](super::traits::Musig2SignerFirstRound::holdouts). Musig2FirstRoundHoldouts { - /// Serialized Schnorr [`XOnlyPublicKey`] of signers whose public nonces - /// we do not have. + /// Serialized Schnorr [`XOnlyPublicKey`](bitcoin::XOnlyPublicKey) of signers whose public + /// nonces we do not have. pubkeys: Vec<[u8; 32]>, }, /// Response for @@ -98,8 +98,8 @@ pub enum ServerMessage { /// Response for /// [`Musig2SignerSecondRound::holdouts`](super::traits::Musig2SignerSecondRound::holdouts). Musig2SecondRoundHoldouts { - /// Serialized Schnorr [`XOnlyPublicKey`] of signers whose partial signatures - /// we do not have for this signing session. + /// Serialized Schnorr [`XOnlyPublicKey`](bitcoin::XOnlyPublicKey) of signers whose partial + /// signatures we do not have for this signing session. pubkeys: Vec<[u8; 32]>, }, @@ -240,7 +240,8 @@ pub enum ClientMessage { /// Session that this server is requesting for. session_id: usize, - /// The serialized [`XOnlyPublicKey`] of the signer whose public nonce this is. + /// The serialized [`XOnlyPublicKey`](bitcoin::XOnlyPublicKey) of the signer whose public + /// nonce this is. pubkey: [u8; 32], /// Serialized public nonce @@ -291,7 +292,8 @@ pub enum ClientMessage { /// Session that this server is requesting for. session_id: usize, - /// The serialized [`XOnlyPublicKey`] of the signer whose public nonce this is. + /// The serialized [`XOnlyPublicKey`](bitcoin::XOnlyPublicKey) of the signer whose public + /// nonce this is. pubkey: [u8; 32], /// That signer's MuSig2 partial signature. diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs index c6d3851b..3012b4cf 100644 --- a/crates/secret-service/src/tests.rs +++ b/crates/secret-service/src/tests.rs @@ -62,6 +62,7 @@ async fn e2e() { // operator signer let op_signer = client.operator_signer(); let pubkey = op_signer.pubkey().await.expect("good response"); + let handles = (0..1000) .map(|_| { let secp_ctx = Secp256k1::verification_only(); From 88fce490e2cea595a986865582b8af777c2e906d Mon Sep 17 00:00:00 2001 From: Azz Date: Tue, 25 Feb 2025 17:40:39 +0000 Subject: [PATCH 30/30] various bug fixes - extending tests to cover api --- crates/secret-service-proto/src/v1/wire.rs | 2 +- crates/secret-service-server/src/bool_arr.rs | 50 +++--- crates/secret-service-server/src/lib.rs | 6 +- .../src/musig2_session_mgr.rs | 33 ++-- crates/secret-service/src/tests.rs | 142 +++++++++++++++++- 5 files changed, 182 insertions(+), 51 deletions(-) diff --git a/crates/secret-service-proto/src/v1/wire.rs b/crates/secret-service-proto/src/v1/wire.rs index 29da98e8..ab0a29eb 100644 --- a/crates/secret-service-proto/src/v1/wire.rs +++ b/crates/secret-service-proto/src/v1/wire.rs @@ -38,7 +38,7 @@ pub enum ServerMessage { /// Response for [`P2PSigner::secret_key`](super::traits::P2PSigner::secret_key). P2PSecretKey { - /// Serialized [`SecretKey`](musig2::secp256k1::SecretKey) + /// Serialized [`SecretKey`](bitcoin::secp256k1::SecretKey) key: [u8; 32], }, diff --git a/crates/secret-service-server/src/bool_arr.rs b/crates/secret-service-server/src/bool_arr.rs index 1604b8d5..3e0da450 100644 --- a/crates/secret-service-server/src/bool_arr.rs +++ b/crates/secret-service-server/src/bool_arr.rs @@ -44,7 +44,7 @@ //! } //! } //! -//! let mut arr = DoubleBoolArray::<2, State>::default(); +//! let mut arr = DoubleBoolArray::<64, State>::default(); //! arr.set(0, State::B); //! arr.set(31, State::C); //! assert_eq!(arr.get(0), State::B); @@ -75,26 +75,30 @@ use std::{ /// - Stores values in N `u64` integers (`8N` bytes total) /// - Provides O(1) access time for get/set operations /// - Implements space-efficient storage with 2 bits per entry -pub struct DoubleBoolArray([u64; N], PhantomData) +pub struct DoubleBoolArray([u64; N / 32], PhantomData) where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, - >::Error: Debug; + >::Error: Debug, + [(); N / 32]:; impl fmt::Debug for DoubleBoolArray where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + fmt::Debug, >::Error: fmt::Debug, + [(); N / 32]:, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { struct DebugValues<'a, const N: usize, T>(&'a DoubleBoolArray) where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, - >::Error: Debug; + >::Error: Debug, + [(); N / 32]:; impl fmt::Debug for DebugValues<'_, N, T> where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + fmt::Debug, >::Error: fmt::Debug, + [(); N / 32]:, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut list = f.debug_list(); @@ -115,9 +119,10 @@ impl Default for DoubleBoolArray where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, >::Error: Debug, + [(); N / 32]:, { fn default() -> Self { - Self([0; N], PhantomData) + Self([0; N / 32], PhantomData) } } @@ -125,11 +130,12 @@ impl DoubleBoolArray where T: Into<(bool, bool)> + TryFrom<(bool, bool)> + Debug, >::Error: Debug, + [(); N / 32]:, { /// Returns the capacity of the array in terms of the number of `(bool, bool)` slots it can /// hold. pub const fn capacity() -> usize { - N * (std::mem::size_of::() * 8 / 2) + N } /// Finds the index of the first slot with the specified value. @@ -148,9 +154,8 @@ where } /// Gets the two boolean values at specified index. - /// Panics if `index >= N * 32`. + /// Panics if `index >= N`. pub fn get(&self, index: usize) -> T { - assert!(index < N * 32, "Index out of bounds"); let chunk_idx = index / 32; let slot = index % 32; let chunk = self.0[chunk_idx]; @@ -161,9 +166,8 @@ where } /// Sets the two boolean values at specified index. - /// Panics if `index >= N * 32`. + /// Panics if `index >= N`. pub fn set(&mut self, index: usize, value: T) { - assert!(index < N * 32, "Index out of bounds"); let chunk_idx = index / 32; let slot = index % 32; let chunk = &mut self.0[chunk_idx]; @@ -215,19 +219,19 @@ mod tests { #[test] fn capacity_calculation() { - assert_eq!(DoubleBoolArray::<1, TestState>::capacity(), 32); - assert_eq!(DoubleBoolArray::<3, TestState>::capacity(), 96); + assert_eq!(DoubleBoolArray::<128, TestState>::capacity(), 128); + assert_eq!(DoubleBoolArray::<32, TestState>::capacity(), 32); } #[test] fn default_initialization() { - let arr = DoubleBoolArray::<2, TestState>::default(); + let arr = DoubleBoolArray::<128, TestState>::default(); assert_eq!(arr.find_first_slot_with(TestState::A), Some(0)); } #[test] fn basic_set_get() { - let mut arr = DoubleBoolArray::<2, TestState>::default(); + let mut arr = DoubleBoolArray::<128, TestState>::default(); arr.set(0, TestState::B); assert_eq!(arr.get(0), TestState::B); @@ -240,22 +244,22 @@ mod tests { } #[test] - #[should_panic(expected = "Index out of bounds")] + #[should_panic(expected = "out of bounds")] fn get_out_of_bounds() { - let arr = DoubleBoolArray::<1, TestState>::default(); - arr.get(32); + let arr = DoubleBoolArray::<128, TestState>::default(); + arr.get(129); } #[test] - #[should_panic(expected = "Index out of bounds")] + #[should_panic(expected = "out of bounds")] fn set_out_of_bounds() { - let mut arr = DoubleBoolArray::<1, TestState>::default(); - arr.set(32, TestState::A); + let mut arr = DoubleBoolArray::<128, TestState>::default(); + arr.set(129, TestState::A); } #[test] fn find_empty_slots() { - let mut arr = DoubleBoolArray::<2, TestState>::default(); + let mut arr = DoubleBoolArray::<64, TestState>::default(); arr.set(5, TestState::B); assert_eq!(arr.find_first_slot_with(TestState::A), Some(0)); @@ -271,7 +275,7 @@ mod tests { #[test] fn slot_independence() { - let mut arr = DoubleBoolArray::<1, TestState>::default(); + let mut arr = DoubleBoolArray::<128, TestState>::default(); arr.set(0, TestState::B); arr.set(1, TestState::C); @@ -284,7 +288,7 @@ mod tests { #[test] fn all_state_combinations() { - let mut arr = DoubleBoolArray::<1, TestState>::default(); + let mut arr = DoubleBoolArray::<128, TestState>::default(); let states = [TestState::A, TestState::B, TestState::C, TestState::D]; for (i, state) in states.iter().enumerate() { diff --git a/crates/secret-service-server/src/lib.rs b/crates/secret-service-server/src/lib.rs index 57c8ef88..f9e90e60 100644 --- a/crates/secret-service-server/src/lib.rs +++ b/crates/secret-service-server/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] //! This module contains the implementation of the secret service server. //! //! This handles networking and communication with clients, but does not implement the traits @@ -249,11 +251,11 @@ where }; let mut sm = musig2_sm.lock().await; - let Ok(write_perm) = sm.new_session(first_round) else { + let Ok(session_id) = sm.new_session(first_round) else { break 'block ServerMessage::OpaqueServerError; }; - ServerMessage::Musig2NewSession(Ok(write_perm.session_id())) + ServerMessage::Musig2NewSession(Ok(session_id)) } ArchivedClientMessage::Musig2Pubkey => ServerMessage::Musig2Pubkey { diff --git a/crates/secret-service-server/src/musig2_session_mgr.rs b/crates/secret-service-server/src/musig2_session_mgr.rs index 0a99e18c..ddba0748 100644 --- a/crates/secret-service-server/src/musig2_session_mgr.rs +++ b/crates/secret-service-server/src/musig2_session_mgr.rs @@ -18,6 +18,7 @@ pub struct Musig2SessionManager where SecondRound: Musig2SignerSecondRound, FirstRound: Musig2SignerFirstRound, + [(); N / 32]:, { /// Tracker is used for tracking whether a session is in first round, /// second round or completed. @@ -31,7 +32,7 @@ where /// /// This is a [`Vec`] because the server doesn't know how big `FirstRound` may be in memory /// so it will heap allocate and try keep this to a minimum. - first_rounds: Vec>>>, + first_rounds: [MaybeUninit>>; N], /// Used to store second rounds of MuSig2 server instances. /// @@ -39,7 +40,7 @@ where /// /// This is a [`Vec`] because the server doesn't know how big `SecondRound` may be in memory so /// it will heap allocate and try keep this to a minimum. - second_rounds: Vec>>>, + second_rounds: [MaybeUninit>>; N], } impl Default @@ -47,12 +48,13 @@ impl Default where SecondRound: Musig2SignerSecondRound, FirstRound: Musig2SignerFirstRound, + [(); N / 32]:, { fn default() -> Self { Self { tracker: DoubleBoolArray::default(), - first_rounds: Vec::new(), - second_rounds: Vec::new(), + first_rounds: std::array::from_fn(|_| MaybeUninit::uninit()), + second_rounds: std::array::from_fn(|_| MaybeUninit::uninit()), } } } @@ -113,29 +115,18 @@ impl Musig2SessionManager, FirstRound: Musig2SignerFirstRound, + [(); N / 32]:, { /// Requests a new session ID from the session manager for a given first round. - pub fn new_session( - &mut self, - first_round: FirstRound, - ) -> Result, Full> { + pub fn new_session(&mut self, first_round: FirstRound) -> Result { let next_empty = self .tracker .find_first_slot_with(SlotState::Empty) .ok_or(Full)?; - let slot = if next_empty <= self.first_rounds.len() { - // we're replacing an existing session - self.first_rounds.get_mut(next_empty).unwrap() - } else { - // we're not replacing any existing session, so we need to grow - self.first_rounds.push(MaybeUninit::uninit()); - self.first_rounds.last_mut().unwrap() - }; - Ok(WritePermission { - slot, - session_id: next_empty, - t: Arc::new(first_round.into()), - }) + let slot = self.first_rounds.get_mut(next_empty).unwrap(); + slot.write(Arc::new(first_round.into())); + self.tracker.set(next_empty, SlotState::FirstRound); + Ok(next_empty) } #[inline] diff --git a/crates/secret-service/src/tests.rs b/crates/secret-service/src/tests.rs index 3012b4cf..5c2ef93a 100644 --- a/crates/secret-service/src/tests.rs +++ b/crates/secret-service/src/tests.rs @@ -1,14 +1,19 @@ use std::{ + cell::RefCell, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, sync::Arc, time::Duration, }; -use bitcoin::key::Secp256k1; -use musig2::secp256k1::Message; +use bitcoin::{ + hashes::Hash, + key::{Keypair, Parity::Even, Secp256k1}, + Txid, XOnlyPublicKey, +}; +use musig2::{secp256k1::Message, FirstRound, KeyAggContext, PartialSignature, SecNonceSpices}; use rand::{thread_rng, Rng}; use secret_service_client::SecretServiceClient; -use secret_service_proto::v1::traits::{OperatorSigner, P2PSigner, SecretService}; +use secret_service_proto::v1::traits::*; use secret_service_server::{ run_server, rustls::{ @@ -17,12 +22,13 @@ use secret_service_server::{ ClientConfig, ServerConfig, }, }; +use strata_bridge_primitives::scripts::taproot::TaprootWitness; use crate::disk::Service; #[tokio::test] async fn e2e() { - let server_addr: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 20000).into(); + let server_addr: SocketAddr = SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 20_000).into(); let server_host = "localhost".to_string(); let cert = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap(); @@ -83,6 +89,134 @@ async fn e2e() { // p2p signer let p2p_signer = client.p2p_signer(); p2p_signer.secret_key().await.expect("good response"); + + let txid = Txid::from_slice(&[0; 32]).unwrap(); + + let sc_preimg = client.stake_chain_preimages(); + sc_preimg + .get_preimg(txid.clone(), 0, 0) + .await + .expect("good response"); + + let wots = client.wots_signer(); + wots.get_160_key(txid.clone(), 0, 0) + .await + .expect("good response"); + wots.get_256_key(txid.clone(), 0, 0) + .await + .expect("good response"); + + let ms2_signer = client.musig2_signer(); + + let signers = (0..2) + .map(|_| Keypair::new_global(&mut thread_rng())) + .collect::>(); + let mut our_first_round = ms2_signer + .new_session( + signers.iter().map(|kp| kp.x_only_public_key().0).collect(), + TaprootWitness::Key, + txid, + 0, + ) + .await + .expect("good response") + .expect("valid keys"); + let our_public_key = ms2_signer.pubkey().await.expect("good response"); + let mut pubkeys = signers + .iter() + .map(|kp| kp.x_only_public_key().0) + .collect::>(); + pubkeys.push(our_public_key); + pubkeys.sort(); + let ctx = KeyAggContext::new(pubkeys.iter().map(|pk| pk.public_key(Even))).unwrap(); + // let agg_pubkey: XOnlyPublicKey = ctx.aggregated_pubkey(); + + let first_rounds = signers + .iter() + .map(|kp| { + let signer_index = pubkeys.binary_search(&kp.x_only_public_key().0).unwrap(); + let spices = SecNonceSpices::new().with_seckey(kp.secret_key()); + FirstRound::new(ctx.clone(), &mut thread_rng(), signer_index, spices) + .unwrap() + .into() + }) + .collect::>>(); + + let our_pub_nonce = our_first_round.our_nonce().await.expect("good response"); + let our_signer_index = pubkeys.binary_search(&our_public_key).unwrap(); + let total_local_signers = first_rounds.len(); + for i in 0..total_local_signers { + let mut fr = first_rounds[i].borrow_mut(); + our_first_round + .receive_pub_nonce(signers[i].x_only_public_key().0, fr.our_public_nonce()) + .await + .expect("good response") + .expect("good nonce"); + fr.receive_nonce(our_signer_index, our_pub_nonce.clone()) + .expect("our nonce to be good"); + for j in 0..total_local_signers { + if i == j || j == our_signer_index { + continue; + } + let other = &first_rounds[j].borrow(); + fr.receive_nonce( + pubkeys + .binary_search(&signers[j].x_only_public_key().0) + .unwrap(), + other.our_public_nonce(), + ) + .expect("other nonce to be good"); + } + } + assert!(our_first_round.is_complete().await.expect("good response")); + let digest = thread_rng().gen(); + let mut our_second_round = our_first_round + .finalize(digest) + .await + .expect("good response") + .expect("good finalize"); + let our_partial_sig = our_second_round + .our_signature() + .await + .expect("good response"); + let second_rounds = first_rounds + .into_iter() + .enumerate() + .map(|(i, fr)| { + fr.into_inner() + .finalize(signers[i].secret_key(), digest) + .unwrap() + .into() + }) + .collect::>>(); + + for i in 0..total_local_signers { + let mut sr = second_rounds[i].borrow_mut(); + // send secret service this signer's partial sig + our_second_round + .receive_signature(signers[i].x_only_public_key().0, sr.our_signature()) + .await + .expect("good response") + .expect("good sig"); + // give secret service's partial sig to this signer + sr.receive_signature(our_signer_index, our_partial_sig.clone()) + .expect("our partial sig to be good"); + // exchange partial sigs with the other local signers + for j in 0..total_local_signers { + if i == j || j == our_signer_index { + continue; + } + let other = &second_rounds[j].borrow(); + sr.receive_signature( + pubkeys + .binary_search(&signers[j].x_only_public_key().0) + .unwrap(), + other.our_signature::(), + ) + .expect("other sig to be good"); + } + } + assert!(our_second_round.is_complete().await.expect("good response")); } /// Dummy certificate verifier that treats any certificate as valid.