diff --git a/.config/nextest.toml b/.config/nextest.toml index 8948928b7..2340d1b69 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -1,2 +1,3 @@ [profile.default] -test-threads = 1 \ No newline at end of file +test-threads = 1 +retries = 2 \ No newline at end of file diff --git a/.github/workflows/validate_pr.yml b/.github/workflows/validate_pr.yml index bf9332ccb..1c017ee98 100644 --- a/.github/workflows/validate_pr.yml +++ b/.github/workflows/validate_pr.yml @@ -26,7 +26,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly components: rustfmt - name: Check Formatting @@ -42,8 +42,9 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly components: clippy + - uses: Swatinem/rust-cache@v2 with: cache-on-failure: "true" @@ -68,7 +69,7 @@ jobs: - name: install rust uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly - uses: swatinem/rust-cache@v2 with: @@ -86,7 +87,7 @@ jobs: runs-on: macos-latest strategy: matrix: - package: [gadget-core, gadget-common, zk-saas-protocol, dfns-cggmp21-protocol, threshold-bls-protocol] + package: [gadget-core, gadget-common, zk-saas-protocol, dfns-cggmp21-protocol, threshold-bls-protocol, zcash-frost-protocol] steps: - name: checkout code uses: actions/checkout@v2 @@ -94,7 +95,7 @@ jobs: - name: install rust uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: nightly - uses: swatinem/rust-cache@v2 with: diff --git a/Cargo.lock b/Cargo.lock index 5daf1fa04..e87892886 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,9 +95,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.10" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b" +checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" dependencies = [ "cfg-if", "getrandom 0.2.12", @@ -785,6 +785,15 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1523,6 +1532,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-crc32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d13f542d70e5b339bf46f6f74704ac052cfd526c58cd87996bd1ef4615b9a0" + [[package]] name = "const-hex" version = "1.11.1" @@ -1853,6 +1868,12 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -2058,6 +2079,7 @@ dependencies = [ "platforms", "rand_core 0.6.4", "rustc_version", + "serde", "subtle", "zeroize", ] @@ -2073,6 +2095,25 @@ dependencies = [ "syn 2.0.51", ] +[[package]] +name = "curve25519-dalek-ml" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d84187ac3a5dddb1466f2f2707336cc6ebe3f658a5b74c0ca7538e07aea7af" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "elliptic-curve 0.13.8", + "fiat-crypto", + "group 0.13.0", + "platforms", + "rand_core 0.6.4", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek-ng" version = "4.1.1" @@ -2244,6 +2285,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive-getters" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2c35ab6e03642397cdda1dd58abbc05d418aef8e36297f336d5aba060fe8df" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive-syn-parse" version = "0.1.5" @@ -2276,6 +2328,7 @@ dependencies = [ "bincode2", "cggmp21", "curv-kzen", + "digest 0.10.7", "frame-support", "futures", "gadget-common", @@ -2283,6 +2336,7 @@ dependencies = [ "hex", "itertools 0.12.1", "log", + "pallet-dkg", "pallet-jobs", "pallet-jobs-rpc-runtime-api", "parity-scale-codec 3.6.9", @@ -2290,8 +2344,10 @@ dependencies = [ "protocol-macros", "rand 0.8.5", "rand_chacha 0.3.1", + "round-based 0.2.1", "sc-client-api", "serde", + "sha2 0.10.8", "sp-api", "sp-application-crypto", "sp-core", @@ -2457,6 +2513,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "document-features" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5282ad69563b5fc40319526ba27e0e7363d552a896f0297d54f767717f9b95" +dependencies = [ + "litrs", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -2573,6 +2638,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed448-goldilocks-plus" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d1a0afcce0f2d28faeda0c9fea990ea5832b9a13a4ecff26d2486fcecc5c014" +dependencies = [ + "elliptic-curve 0.13.8", + "hex", + "rand_core 0.6.4", + "serde", + "sha3 0.10.8", + "subtle", + "zeroize", +] + [[package]] name = "ed448-goldilocks-plus" version = "0.11.2" @@ -3383,7 +3463,7 @@ dependencies = [ [[package]] name = "frost-core" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "byteorder", "debugless-unwrap", @@ -3398,38 +3478,86 @@ dependencies = [ "zeroize", ] +[[package]] +name = "frost-core" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "byteorder", + "const-crc32", + "debugless-unwrap", + "derive-getters", + "document-features", + "hex", + "itertools 0.12.1", + "postcard", + "rand_core 0.6.4", + "serde", + "serdect", + "subtle", + "thiserror", + "visibility", + "zeroize", +] + [[package]] name = "frost-ed25519" version = "1.0.0-rc.0" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "curve25519-dalek 4.1.1", - "frost-core", + "frost-core 0.6.1", "parity-scale-codec 3.6.9", "rand_core 0.6.4", "sha2 0.10.8", "subtle", ] +[[package]] +name = "frost-ed25519" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "curve25519-dalek-ml", + "document-features", + "frost-core 1.0.0-rc.0", + "frost-rerandomized", + "rand_core 0.6.4", + "sha2 0.10.8", +] + [[package]] name = "frost-ed448" version = "1.0.0-rc.0" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ - "ed448-goldilocks-plus", - "frost-core", + "ed448-goldilocks-plus 0.11.2 (git+https://github.com/webb-tools/Ed448-Goldilocks)", + "frost-core 0.6.1", "parity-scale-codec 3.6.9", "rand_core 0.6.4", "sha3 0.10.8", "subtle", ] +[[package]] +name = "frost-ed448" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "document-features", + "ed448-goldilocks-plus 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)", + "frost-core 1.0.0-rc.0", + "frost-rerandomized", + "rand_core 0.6.4", + "sha3 0.10.8", +] + [[package]] name = "frost-p256" version = "1.0.0-rc.0" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ - "frost-core", + "frost-core 0.6.1", "p256 0.13.2", "parity-scale-codec 3.6.9", "rand_core 0.6.4", @@ -3437,12 +3565,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "frost-p256" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "document-features", + "frost-core 1.0.0-rc.0", + "frost-rerandomized", + "p256 0.13.2", + "rand_core 0.6.4", + "sha2 0.10.8", +] + [[package]] name = "frost-p384" version = "1.0.0-rc.0" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ - "frost-core", + "frost-core 0.6.1", "p384", "parity-scale-codec 3.6.9", "rand_core 0.6.4", @@ -3450,25 +3591,62 @@ dependencies = [ "subtle", ] +[[package]] +name = "frost-p384" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "document-features", + "frost-core 1.0.0-rc.0", + "frost-rerandomized", + "p384", + "rand_core 0.6.4", + "sha2 0.10.8", +] + +[[package]] +name = "frost-rerandomized" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "derive-getters", + "document-features", + "frost-core 1.0.0-rc.0", + "rand_core 0.6.4", +] + [[package]] name = "frost-ristretto255" version = "1.0.0-rc.0" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "curve25519-dalek 4.1.1", - "frost-core", + "frost-core 0.6.1", "parity-scale-codec 3.6.9", "rand_core 0.6.4", "sha2 0.10.8", "subtle", ] +[[package]] +name = "frost-ristretto255" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "curve25519-dalek 4.1.1", + "document-features", + "frost-core 1.0.0-rc.0", + "frost-rerandomized", + "rand_core 0.6.4", + "sha2 0.10.8", +] + [[package]] name = "frost-secp256k1" version = "1.0.0-rc.0" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ - "frost-core", + "frost-core 0.6.1", "k256", "parity-scale-codec 3.6.9", "rand_core 0.6.4", @@ -3476,6 +3654,19 @@ dependencies = [ "subtle", ] +[[package]] +name = "frost-secp256k1" +version = "1.0.0-rc.0" +source = "git+https://github.com/LIT-Protocol/frost.git#f1e6ab1f8ed163f64b358c9d3cfef185a29513f8" +dependencies = [ + "document-features", + "frost-core 1.0.0-rc.0", + "frost-rerandomized", + "k256", + "rand_core 0.6.4", + "sha2 0.10.8", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -3689,6 +3880,7 @@ dependencies = [ "parking_lot 0.12.1", "protocol-macros", "round-based 0.1.7", + "round-based 0.2.1", "sc-client-api", "sc-network", "sc-network-common", @@ -4006,6 +4198,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.11.2" @@ -4030,7 +4231,7 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" dependencies = [ - "ahash 0.8.10", + "ahash 0.8.9", ] [[package]] @@ -4039,7 +4240,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" dependencies = [ - "ahash 0.8.10", + "ahash 0.8.9", "allocator-api2", "serde", ] @@ -4053,6 +4254,20 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.3.3" @@ -4699,6 +4914,7 @@ dependencies = [ "elliptic-curve 0.13.8", "once_cell", "sha2 0.10.8", + "signature 2.2.0", ] [[package]] @@ -5327,6 +5543,12 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.11" @@ -6239,6 +6461,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "primeorder 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.10.8", ] [[package]] @@ -6249,6 +6472,7 @@ dependencies = [ "ecdsa 0.16.9", "elliptic-curve 0.13.8", "primeorder 0.13.6 (git+https://github.com/LIT-Protocol/elliptic-curves.git)", + "sha2 0.10.8", ] [[package]] @@ -6309,19 +6533,19 @@ dependencies = [ [[package]] name = "pallet-dkg" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "digest 0.10.7", "elliptic-curve 0.13.8", "frame-support", "frame-system", - "frost-core", - "frost-ed25519", - "frost-ed448", - "frost-p256", - "frost-p384", - "frost-ristretto255", - "frost-secp256k1", + "frost-core 0.6.1", + "frost-ed25519 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git?branch=main)", + "frost-ed448 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git?branch=main)", + "frost-p256 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git?branch=main)", + "frost-p384 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git?branch=main)", + "frost-ristretto255 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git?branch=main)", + "frost-secp256k1 1.0.0-rc.0 (git+https://github.com/webb-tools/tangle.git?branch=main)", "generic-ec", "generic-ec-zkp", "hex", @@ -6351,7 +6575,7 @@ dependencies = [ [[package]] name = "pallet-jobs" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "frame-benchmarking", "frame-support", @@ -6368,7 +6592,7 @@ dependencies = [ [[package]] name = "pallet-jobs-rpc-runtime-api" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "parity-scale-codec 3.6.9", "sp-api", @@ -6399,7 +6623,7 @@ dependencies = [ [[package]] name = "pallet-zksaas" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "frame-benchmarking", "frame-support", @@ -6843,6 +7067,7 @@ checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" dependencies = [ "cobs", "embedded-io", + "heapless", "serde", ] @@ -7462,9 +7687,9 @@ checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" [[package]] name = "rayon" -version = "1.9.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" dependencies = [ "either", "rayon-core", @@ -8875,7 +9100,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "772575a524feeb803e5b0fcbc6dd9f367e579488197c94c6e4023aad2305774d" dependencies = [ - "ahash 0.8.10", + "ahash 0.8.9", "cfg-if", "hashbrown 0.13.2", ] @@ -10137,7 +10362,7 @@ name = "sp-trie" version = "22.0.0" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0#c8d2251cafadc108ba2f1f8a3208dc547ff38901" dependencies = [ - "ahash 0.8.10", + "ahash 0.8.9", "hash-db", "hashbrown 0.13.2", "lazy_static", @@ -10276,7 +10501,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" dependencies = [ - "ahash 0.8.10", + "ahash 0.8.9", "atoi", "byteorder", "bytes", @@ -10823,7 +11048,7 @@ dependencies = [ [[package]] name = "tangle-crypto-primitives" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "parity-scale-codec 3.6.9", "scale-info", @@ -10833,7 +11058,7 @@ dependencies = [ [[package]] name = "tangle-primitives" version = "0.6.1" -source = "git+https://github.com/webb-tools/tangle.git?branch=main#bfa60f4d9a9118db6e4d58bfdb1c0a1e7ee02866" +source = "git+https://github.com/webb-tools/tangle.git?branch=main#81c5c468ec7b8a80d8b9c585412da3670b8ebcd3" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -10844,7 +11069,7 @@ dependencies = [ "ark-std", "ethabi 15.0.0", "frame-support", - "frost-core", + "frost-core 0.6.1", "log", "parity-scale-codec 3.6.9", "scale-info", @@ -11702,6 +11927,17 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "visibility" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3fd98999db9227cf28e59d83e1f120f42bc233d4b152e8fab9bc87d5bb1e0f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.51", +] + [[package]] name = "void" version = "1.0.2" @@ -11829,9 +12065,9 @@ checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "wasm-encoder" -version = "0.201.0" +version = "0.200.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a" +checksum = "b9e3fb0c8fbddd78aa6095b850dfeedbc7506cf5f81e633f69cf8f2333ab84b9" dependencies = [ "leb128", ] @@ -12345,9 +12581,9 @@ dependencies = [ [[package]] name = "wast" -version = "201.0.0" +version = "200.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ef6e1ef34d7da3e2b374fd2b1a9c0227aff6cad596e1b24df9b58d0f6222faa" +checksum = "d1810d14e6b03ebb8fb05eef4009ad5749c989b65197d83bce7de7172ed91366" dependencies = [ "bumpalo", "leb128", @@ -12358,9 +12594,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.201.0" +version = "1.200.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453d5b37a45b98dee4f4cb68015fc73634d7883bbef1c65e6e9c78d454cf3f32" +checksum = "776cbd10e217f83869beaa3f40e312bb9e91d5eee29bbf6f560db1261b6a4c3d" dependencies = [ "wast", ] @@ -12378,7 +12614,7 @@ dependencies = [ [[package]] name = "webb" version = "0.8.4" -source = "git+https://github.com/webb-tools/webb-rs.git?branch=main#118801febf8bc221fc617622ca0cf994b1f05e15" +source = "git+https://github.com/webb-tools/webb-rs.git?branch=main#05c3441fe2c957c2eaa473f3f59bd593c396b521" dependencies = [ "async-trait", "hex", @@ -12856,6 +13092,55 @@ dependencies = [ "time", ] +[[package]] +name = "zcash-frost-protocol" +version = "0.1.0" +dependencies = [ + "async-trait", + "bincode2", + "cggmp21", + "curv-kzen", + "digest 0.10.7", + "frame-support", + "frost-core 1.0.0-rc.0", + "frost-ed25519 1.0.0-rc.0 (git+https://github.com/LIT-Protocol/frost.git)", + "frost-ed448 1.0.0-rc.0 (git+https://github.com/LIT-Protocol/frost.git)", + "frost-p256 1.0.0-rc.0 (git+https://github.com/LIT-Protocol/frost.git)", + "frost-p384 1.0.0-rc.0 (git+https://github.com/LIT-Protocol/frost.git)", + "frost-rerandomized", + "frost-ristretto255 1.0.0-rc.0 (git+https://github.com/LIT-Protocol/frost.git)", + "frost-secp256k1 1.0.0-rc.0 (git+https://github.com/LIT-Protocol/frost.git)", + "futures", + "gadget-common", + "gadget-core", + "hex", + "itertools 0.12.1", + "log", + "pallet-dkg", + "pallet-jobs", + "pallet-jobs-rpc-runtime-api", + "parity-scale-codec 3.6.9", + "parking_lot 0.12.1", + "protocol-macros", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "round-based 0.2.1", + "sc-client-api", + "serde", + "sha2 0.10.8", + "sp-api", + "sp-application-crypto", + "sp-core", + "sp-io", + "sp-runtime", + "tangle-primitives", + "test-utils", + "thiserror", + "tokio", + "udigest", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index c04075ada..302fb5edb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "test-gadget", "protocols/dfns-cggmp21", "protocols/zk-saas", + "protocols/zcash-frost", "protocols/bls", "protocols/stub", "test-utils", @@ -21,6 +22,7 @@ stub-protocol = { path = "./protocols/stub" } test-utils = { path = "./test-utils" } protocol-macros = { path = "./protocol-macros" } dfns-cggmp21-protocol = { path = "./protocols/dfns-cggmp21" } +zcash-frost-protocol = { path = "./protocols/zcash-frost" } snowbridge-milagro-bls = { git = "https://github.com/Snowfork/milagro_bls", default-features = false, rev = "43a5d480ed6e3b83de4cf54888680d51604199e6" } gennaro-dkg = { git = "https://github.com/mikelodder7/gennaro-dkg.git", default-features = false } @@ -29,14 +31,35 @@ pallet-jobs = { git = "https://github.com/webb-tools/tangle.git", branch = "main pallet-dkg = { git = "https://github.com/webb-tools/tangle.git", branch = "main" } pallet-zksaas = { git = "https://github.com/webb-tools/tangle.git", branch = "main" } tangle-primitives = { git = "https://github.com/webb-tools/tangle.git", branch = "main" } +# pallet-jobs-rpc-runtime-api = { path = "../tangle/pallets/jobs/rpc/runtime-api" } +# pallet-jobs = { path = "../tangle/pallets/jobs" } +# pallet-dkg = { path = "../tangle/pallets/dkg" } +# pallet-zksaas = { path = "../tangle/pallets/zksaas" } +# tangle-primitives = { path = "../tangle/primitives" } webb = { git = "https://github.com/webb-tools/webb-rs.git", default-features = false, branch = "main" } subxt-signer = { version = "0.31.0", default-features = false } multi-party-ecdsa = { git = "https://github.com/webb-tools/cggmp-threshold-ecdsa/" } + +# Round-based dependencies round-based = { git = "https://github.com/webb-tools/round-based-protocol", features = [] } +round-based-21 = { package = "round-based", version = "0.2.1" } +round-based-zengo = { package = "round-based", git = "https://github.com/ZenGo-X/round-based-protocol", features = ["derive"]} + + curv = { package = "curv-kzen", version = "0.10.0" } +digest = "0.10" dfns-cggmp21 = { package = "cggmp21", version = "0.1.1", default-features = false } +udigest = { version = "0.1", features = ["std", "derive"]} +frost-core = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-ed25519 = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-ed448 = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-p256 = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-p384 = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-ristretto255 = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-secp256k1 = { git = "https://github.com/LIT-Protocol/frost.git" } +frost-rerandomized = { git = "https://github.com/LIT-Protocol/frost.git" } bls12_381_plus = "0.8.13" sp-core = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } @@ -131,6 +154,9 @@ k256 = "0.13.2" anyhow = "1.0.79" libsecp256k1 = "0.7.1" rayon = { version = "1.8.0" } +thiserror = { version = "1.0" } substrate-prometheus-endpoint = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } lazy_static = "1.4.0" sqlx = "=0.7.3" +postcard = "1.0.8" +sha2 = "0.10.8" diff --git a/README.md b/README.md index 3ec9b75e6..f4c1a9080 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,17 @@ # Gadget -## Introduction -A **gadget** is a peer on a network that queries a blockchain for job information, then, performs work based on these on-chain jobs cooperatively with other gadgets. -There are different types of jobs, such as threshold signatures, zero-knowledge proofs, and more. Each job type has a corresponding **protocol** that the gadgets use to perform the work. -These protocols, under the hood, each make use of an applied cryptographic protocol over a network, and work towards submitting job outputs to the blockchain. - +This repo contains code for MPC and other restaking service gadgets. A gadget is a service that listens to a job management system (such as a blockchain w/ onchain job management logic) and communicates with other service providers using a peer to peer or alternative networking stack. Currently, the main services the gadget implements are multi-party computation services such as threshold signature MPCs and an MPC proving service for Groth16 zkSNARKs. +- [x] [DFNS CGGMP21](https://github.com/dfns/cggmp21/tree/m/cggmp21) +- [x] [Threshold BLS](https://github.com/mikelodder7/blsful) +- [x] [LIT Protocol fork of ZCash Frost](https://github.com/LIT-Protocol/frost) +- [x] [Groth16 ZK-SaaS](https://github.com/webb-tools/zk-SaaS) ## Design +The core library is `gadget-core`. The core library allows gadgets to hold standardization of use across different blockchains that implement a compatible job management and submission infrastructure. All gadgets should implement the relevant traits from `gadget-core`, which implement job allocation, completion, and submission. Currently, gadgets expect to receive `FinalityNotifications` and `BlockImportNotifications` so blockchains with finality are mainly targetted. + +Currently the repo is built around Substrate blockchain logic and networking. The job system implemented by [Tangle](https://github.com/webb-tools/tangle) drives the current job allocation mechanism. Validators of a Substrate chain implementing Tangle's runtime pallets execute jobs assigned to them from an onchain job submission system and use the underlying Substrate p2p layer to communicate with other service peers. + The core library is `gadget-core`. The core library allows gadgets to hold standardization of use across different blockchains. The core library is the base of all gadgets, and expects to receive `FinalityNotifications` and `BlockImportNotifications`. Once such blockchain is a substrate blockchain. This is where `gadget-common` comes into play. The `gadget-common` is the `core-gadget` endowed with a connection to a substrate blockchain, a networking layer to communicate with other gadgets, and a `SubstrateGadget` that has application-specific logic. Since `gadget-common` allows varying connections to a substrate blockchain and differing network layers, we thus design above it various *protocols*. Some example protocols are `zk-saas-protocol`, `dfns-cggmp21-protocol`, `threshold-bls-protocol`, and `stub-protocol` (where the latter is for getting a bare minimum skeleton of a protocol crate setup). @@ -15,8 +19,7 @@ These protocols are endowed with the same functionalities as the `gadget-common` For more information on how to create a new protocol, see the README.md in the `protocols/stub` directory [here](protocols/stub/README.md). ## Testing -`cargo nextest run` is required to run tests, since 1-program per-program space is required for tests due to the nature of the use of static variables in test-only contexts. - +`RUST_LOG=debug cargo nextest run` is required to run tests, since 1-program per-program space is required for tests due to the nature of the use of static variables in test-only contexts. The `RUST_LOG=debug` flag is optional but useful for debugging. ## Troubleshooting #### GMP Issues The linking phase may fail due to not finding libgmp (i.e., "could not find library -lgmp") when building on a mac M1. To fix this problem, run: diff --git a/gadget-common/Cargo.toml b/gadget-common/Cargo.toml index 7af857c0c..bf76df48c 100644 --- a/gadget-common/Cargo.toml +++ b/gadget-common/Cargo.toml @@ -41,6 +41,7 @@ subxt-signer = { workspace = true, features = ["subxt", "sr25519"] } anyhow = { workspace = true } futures = { workspace = true } round-based = { workspace = true } +round-based-21 = { workspace = true, features = ["derive"]} sp-api = { workspace = true } sp-io = { workspace = true } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } diff --git a/gadget-common/src/channels.rs b/gadget-common/src/channels.rs index a9f929ed2..9db6ab554 100644 --- a/gadget-common/src/channels.rs +++ b/gadget-common/src/channels.rs @@ -3,9 +3,11 @@ use crate::gadget::message::{GadgetProtocolMessage, UserID}; use crate::gadget::network::Network; use crate::gadget::work_manager::WorkManager; +use crate::prelude::DebugLogger; use futures::StreamExt; use gadget_core::job_manager::WorkManagerInterface; use round_based::Msg; +use round_based_21::{Incoming, MessageDestination, MessageType, MsgId, Outgoing, PartyIndex}; use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use sp_core::ecdsa; @@ -13,16 +15,11 @@ use std::collections::HashMap; use std::sync::Arc; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; -#[derive(Serialize, Deserialize, Debug)] -pub enum SplitChannelMessage { - Channel1(C1), - Channel2(C2), -} - +#[allow(clippy::too_many_arguments)] pub fn create_job_manager_to_async_protocol_channel_split< N: Network + 'static, - C1: Serialize + DeserializeOwned + HasSenderAndReceiver + Send + 'static, - C2: Serialize + DeserializeOwned + HasSenderAndReceiver + Send + 'static, + C1: Serialize + DeserializeOwned + MaybeSenderReceiver + Send + 'static, + C2: Serialize + DeserializeOwned + MaybeSenderReceiver + Send + 'static, >( mut rx_gadget: UnboundedReceiver, associated_block_id: ::Clock, @@ -30,7 +27,9 @@ pub fn create_job_manager_to_async_protocol_channel_split< associated_session_id: ::SessionID, associated_task_id: ::TaskID, user_id_mapping: Arc>, + my_account_id: ecdsa::Public, network: N, + logger: DebugLogger, ) -> ( futures::channel::mpsc::UnboundedSender, futures::channel::mpsc::UnboundedReceiver>, @@ -39,25 +38,29 @@ pub fn create_job_manager_to_async_protocol_channel_split< ) { let (tx_to_async_proto_1, rx_for_async_proto_1) = futures::channel::mpsc::unbounded(); let (tx_to_async_proto_2, rx_for_async_proto_2) = tokio::sync::mpsc::unbounded_channel(); - + let logger_outgoing = logger.clone(); // Take the messages from the gadget and send them to the async protocol tokio::task::spawn(async move { while let Some(msg) = rx_gadget.recv().await { - match bincode2::deserialize::>(&msg.payload) { + match bincode2::deserialize::>(&msg.payload) { Ok(msg) => match msg { - SplitChannelMessage::Channel1(msg) => { + MultiplexedChannelMessage::Channel1(msg) => { if tx_to_async_proto_1.unbounded_send(Ok(msg)).is_err() { - log::error!(target: "gadget", "Failed to send message to protocol"); + logger.error("Failed to send message to C1 protocol"); } } - SplitChannelMessage::Channel2(msg) => { + MultiplexedChannelMessage::Channel2(msg) => { if tx_to_async_proto_2.send(msg).is_err() { - log::error!(target: "gadget", "Failed to send message to protocol"); + logger.error("Failed to send message to C2 protocol"); } } + + _ => { + unreachable!("We only have two channels") + } }, Err(err) => { - log::error!(target: "gadget", "Failed to deserialize message: {err:?}"); + logger.error(format!("Failed to deserialize message: {err:?}")); } } } @@ -67,59 +70,63 @@ pub fn create_job_manager_to_async_protocol_channel_split< let (tx_to_outbound_2, mut rx_to_outbound_2) = tokio::sync::mpsc::unbounded_channel::(); let network_clone = network.clone(); let user_id_mapping_clone = user_id_mapping.clone(); + let my_user_id = user_id_mapping + .iter() + .find_map(|(user_id, account_id)| { + if *account_id == my_account_id { + Some(*user_id) + } else { + None + } + }) + .expect("Failed to find my user id"); + // Take the messages the async protocol sends to the outbound channel and send them to the gadget tokio::task::spawn(async move { - let offline_task = async move { + let logger = &logger_outgoing; + let channel_1_task = async move { while let Some(msg) = rx_to_outbound_1.next().await { - let from = msg.sender(); - let to = msg.receiver(); - let (to_account_id, from_account_id) = - get_to_and_from_account_id(&user_id_mapping_clone, from, to); - let msg = SplitChannelMessage::::Channel1(msg); - let msg = GadgetProtocolMessage { + if let Err(err) = wrap_message_and_forward_to_network::<_, C1, C2, (), _>( + msg, + &network, + &user_id_mapping, + my_user_id, associated_block_id, associated_session_id, associated_retry_id, - task_hash: associated_task_id, - from, - to, - payload: bincode2::serialize(&msg).expect("Failed to serialize message"), - from_network_id: from_account_id, - to_network_id: to_account_id, - }; - - if let Err(err) = network.send_message(msg).await { - log::error!(target:"gadget", "Failed to send message to outbound: {err:?}"); + associated_task_id, + MultiplexedChannelMessage::Channel1, + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); } } }; - let voting_task = async move { + let channel_2_task = async move { while let Some(msg) = rx_to_outbound_2.recv().await { - let from = msg.sender(); - let to = msg.receiver(); - let (to_account_id, from_account_id) = - get_to_and_from_account_id(&user_id_mapping, from, to); - let msg = SplitChannelMessage::::Channel2(msg); - let msg = GadgetProtocolMessage { + if let Err(err) = wrap_message_and_forward_to_network::<_, C1, C2, (), _>( + msg, + &network_clone, + &user_id_mapping_clone, + my_user_id, associated_block_id, associated_session_id, associated_retry_id, - task_hash: associated_task_id, - from, - to, - payload: bincode2::serialize(&msg).expect("Failed to serialize message"), - from_network_id: from_account_id, - to_network_id: to_account_id, - }; - - if let Err(err) = network_clone.send_message(msg).await { - log::error!(target:"gadget", "Failed to send message to outbound: {err:?}"); + associated_task_id, + MultiplexedChannelMessage::Channel2, + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); } } }; - tokio::join!(offline_task, voting_task); + tokio::join!(channel_1_task, channel_2_task); }); ( @@ -130,10 +137,11 @@ pub fn create_job_manager_to_async_protocol_channel_split< ) } -fn get_to_and_from_account_id( +pub fn get_to_and_from_account_id( mapping: &HashMap, from: UserID, to: Option, + logger: &DebugLogger, ) -> (Option, Option) { let from_account_id = mapping.get(&from).cloned(); let to_account_id = if let Some(to) = to { @@ -142,29 +150,682 @@ fn get_to_and_from_account_id( None }; + logger.trace(format!( + "From (mapped): {:?}, To: {:?}", + from_account_id, to_account_id + )); + (to_account_id, from_account_id) } -pub trait HasSenderAndReceiver { - fn sender(&self) -> UserID; - fn receiver(&self) -> Option; +impl MaybeSenderReceiver for Msg { + fn maybe_sender(&self) -> MaybeSender { + MaybeSender::SomeoneElse(self.sender as UserID) + } + + fn maybe_receiver(&self) -> MaybeReceiver { + match self.receiver { + None => MaybeReceiver::Broadcast, + Some(i) => MaybeReceiver::P2P(i as UserID), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum MultiplexedChannelMessage { + Channel1(C1), + Channel2(C2), + Channel3(C3), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct VotingMessage { + pub from: UserID, + pub to: Option, + pub payload: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PublicKeyGossipMessage { + pub from: UserID, + pub to: Option, + pub signature: Vec, + pub id: sp_core::ecdsa::Public, +} + +/// All possible senders of a message +#[derive(Debug, Default, Serialize, Deserialize)] +pub enum MaybeSender { + /// We are the sender of the message + Myself, + /// The sender is someone else + /// it could also be us, double check the [`UserID`] + SomeoneElse(UserID), + /// The sender is unknown. + #[default] + Unknown, +} + +impl MaybeSender { + /// Returns `true` if the maybe sender is [`Myself`]. + /// + /// [`Myself`]: MaybeSender::Myself + #[must_use] + pub fn is_myself(&self) -> bool { + matches!(self, Self::Myself) + } + + /// Returns `true` if the maybe sender is [`Myself`]. + /// Or if the sender is [`SomeoneElse`] but the [`UserID`] is the same as `my_user_id` + /// + /// [`Myself`]: MaybeSender::Myself + /// [`SomeoneElse`]: MaybeSender::SomeoneElse + #[must_use] + pub fn is_myself_check(&self, my_user_id: UserID) -> bool { + match self { + Self::Myself => true, + Self::SomeoneElse(id) if (*id == my_user_id) => true, + _ => false, + } + } + + /// Returns `true` if the maybe sender is [`SomeoneElse`]. + /// + /// [`SomeoneElse`]: MaybeSender::SomeoneElse + #[must_use] + pub fn is_someone_else(&self) -> bool { + matches!(self, Self::SomeoneElse(..)) + } + + /// Returns `true` if the maybe sender is [`Unknown`]. + /// + /// [`Unknown`]: MaybeSender::Unknown + #[must_use] + pub fn is_unknown(&self) -> bool { + matches!(self, Self::Unknown) + } + + /// Returns the sender as [`UserID`] if it is knwon. + #[must_use] + pub fn as_user_id(&self) -> Option { + match self { + Self::Myself => None, + Self::SomeoneElse(id) => Some(*id), + Self::Unknown => None, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub enum MaybeReceiver { + /// The message is broadcasted to everyone + Broadcast, + /// The message is sent to a specific party + P2P(UserID), + /// The receiver is us. + Myself, + /// The receiver is unknown. + #[default] + Unknown, +} + +impl MaybeReceiver { + /// Returns `true` if the maybe receiver is [`Broadcast`]. + /// + /// [`Broadcast`]: MaybeReceiver::Broadcast + #[must_use] + pub fn is_broadcast(&self) -> bool { + matches!(self, Self::Broadcast) + } + + /// Returns `true` if the maybe receiver is [`P2P`]. + /// + /// [`P2P`]: MaybeReceiver::P2P + #[must_use] + pub fn is_p2_p(&self) -> bool { + matches!(self, Self::P2P(..)) + } + + /// Returns `true` if the maybe receiver is [`Myself`]. + /// + /// [`Myself`]: MaybeReceiver::Myself + #[must_use] + pub fn is_myself(&self) -> bool { + matches!(self, Self::Myself) + } + + /// Returns `true` if the maybe receiver is [`Myself`] + /// Or if the receiver is [`P2P`] but the [`UserID`] is the same as `my_user_id` + /// + /// [`Myself`]: MaybeReceiver::Myself + /// [`P2P`]: MaybeReceiver::P2P + #[must_use] + pub fn is_myself_check(&self, my_user_id: UserID) -> bool { + match self { + Self::Myself => true, + Self::P2P(id) if (*id == my_user_id) => true, + _ => false, + } + } + + /// Returns `true` if the maybe receiver is [`Unknown`]. + /// + /// [`Unknown`]: MaybeReceiver::Unknown + #[must_use] + pub fn is_unknown(&self) -> bool { + matches!(self, Self::Unknown) + } + + /// Returns the receiver as [`UserID`] if it is knwon. + #[must_use] + pub fn as_user_id(&self) -> Option { + match self { + Self::Broadcast => None, + Self::P2P(id) => Some(*id), + Self::Myself => None, + Self::Unknown => None, + } + } +} + +pub trait InnerMessage { + type Inner: Serialize + DeserializeOwned + Send + 'static; + fn inner_message(self) -> Self::Inner; +} + +pub trait InnerMessageFromInbound: Sized + InnerMessage { + fn from_inbound( + id: MsgId, + sender: PartyIndex, + msg_type: MessageType, + msg: ::Inner, + ) -> Self; +} + +impl InnerMessage for Outgoing { + type Inner = M; + + fn inner_message(self) -> Self::Inner { + self.msg + } +} + +impl InnerMessage for Incoming { + type Inner = M; + + fn inner_message(self) -> Self::Inner { + self.msg + } +} + +/// A Simple trait to extract the sender and the receiver from a message +pub trait MaybeSenderReceiver { + fn maybe_sender(&self) -> MaybeSender; + fn maybe_receiver(&self) -> MaybeReceiver; +} + +impl MaybeSenderReceiver for PublicKeyGossipMessage { + fn maybe_sender(&self) -> MaybeSender { + MaybeSender::SomeoneElse(self.from) + } + fn maybe_receiver(&self) -> MaybeReceiver { + match self.to { + Some(id) => MaybeReceiver::P2P(id), + None => MaybeReceiver::Broadcast, + } + } +} + +impl MaybeSenderReceiver for VotingMessage { + fn maybe_sender(&self) -> MaybeSender { + MaybeSender::SomeoneElse(self.from) + } + fn maybe_receiver(&self) -> MaybeReceiver { + match self.to { + Some(id) => MaybeReceiver::P2P(id), + None => MaybeReceiver::Broadcast, + } + } +} + +impl MaybeSenderReceiver for Outgoing { + fn maybe_sender(&self) -> MaybeSender { + MaybeSender::Myself + } + + fn maybe_receiver(&self) -> MaybeReceiver { + match self.recipient { + MessageDestination::AllParties => MaybeReceiver::Broadcast, + MessageDestination::OneParty(i) => MaybeReceiver::P2P(i as UserID), + } + } } -impl HasSenderAndReceiver for Msg { - fn sender(&self) -> UserID { - self.sender as UserID +impl MaybeSenderReceiver for Incoming { + fn maybe_sender(&self) -> MaybeSender { + MaybeSender::SomeoneElse(self.sender as UserID) } - fn receiver(&self) -> Option { - self.receiver.map(|r| r as UserID) + + fn maybe_receiver(&self) -> MaybeReceiver { + match self.msg_type { + MessageType::Broadcast => MaybeReceiver::Broadcast, + MessageType::P2P => MaybeReceiver::Myself, + } + } +} + +impl InnerMessageFromInbound for Incoming { + fn from_inbound( + id: MsgId, + sender: PartyIndex, + msg_type: MessageType, + msg: ::Inner, + ) -> Self { + Incoming { + id, + sender, + msg_type, + msg, + } } } -impl HasSenderAndReceiver for () { - fn sender(&self) -> UserID { - unimplemented!("Stub implementation") +impl MaybeSenderReceiver for () { + fn maybe_sender(&self) -> MaybeSender { + MaybeSender::Unknown } - fn receiver(&self) -> Option { - unimplemented!("Stub implementation") + fn maybe_receiver(&self) -> MaybeReceiver { + MaybeReceiver::Unknown } } + +pub type DuplexedChannel = ( + futures::channel::mpsc::UnboundedSender, + futures::channel::mpsc::UnboundedReceiver>, + futures::channel::mpsc::UnboundedSender, + futures::channel::mpsc::UnboundedReceiver, +); + +#[allow(clippy::too_many_arguments)] +pub fn create_job_manager_to_async_protocol_channel_split_io< + N: Network + 'static, + C2: Serialize + DeserializeOwned + MaybeSenderReceiver + Send + 'static, + O: InnerMessage + MaybeSenderReceiver + Send + 'static, + I: InnerMessage + InnerMessageFromInbound + MaybeSenderReceiver + Send + 'static, +>( + mut rx_gadget: UnboundedReceiver, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + user_id_mapping: Arc>, + my_account_id: ecdsa::Public, + network: N, + logger: DebugLogger, +) -> DuplexedChannel { + let (tx_to_async_proto_1, rx_for_async_proto_1) = futures::channel::mpsc::unbounded(); + let (tx_to_async_proto_2, rx_for_async_proto_2) = futures::channel::mpsc::unbounded(); + let logger_outgoing = logger.clone(); + + // Take the messages from the gadget and send them to the async protocol + tokio::task::spawn(async move { + let mut id = 0; + while let Some(msg_orig) = rx_gadget.recv().await { + if msg_orig.payload.is_empty() { + logger.warn(format!( + "Received empty message from Peer {}", + msg_orig.from + )); + continue; + } + match bincode2::deserialize::>( + &msg_orig.payload, + ) { + Ok(msg) => match msg { + MultiplexedChannelMessage::Channel1(msg) => { + logger.info(format!( + "Received message from {} as {:?}", + msg_orig.from, msg_orig.to + )); + let msg_type = if msg_orig.to.is_some() { + MessageType::P2P + } else { + MessageType::Broadcast + }; + + let incoming = + I::from_inbound(id, msg_orig.from as PartyIndex, msg_type, msg); + + if tx_to_async_proto_1.unbounded_send(Ok(incoming)).is_err() { + logger.error("Failed to send Incoming message to protocol"); + } + + id += 1; + } + MultiplexedChannelMessage::Channel2(msg) => { + if tx_to_async_proto_2.unbounded_send(msg).is_err() { + logger.error("Failed to send C2 message to protocol"); + } + } + _ => { + unreachable!("We only have two channels") + } + }, + Err(err) => { + logger.error(format!("Failed to deserialize message: {err:?}")); + } + } + } + }); + + let (tx_to_outbound_1, mut rx_to_outbound_1) = futures::channel::mpsc::unbounded::(); + let (tx_to_outbound_2, mut rx_to_outbound_2) = futures::channel::mpsc::unbounded::(); + let network_clone = network.clone(); + let user_id_mapping_clone = user_id_mapping.clone(); + let my_user_id = user_id_mapping + .iter() + .find_map(|(user_id, account_id)| { + if *account_id == my_account_id { + Some(*user_id) + } else { + None + } + }) + .expect("Failed to find my user id"); + // Take the messages from the async protocol and send them to the gadget + tokio::task::spawn(async move { + let logger = &logger_outgoing; + let channel_1_task = async move { + while let Some(msg) = rx_to_outbound_1.next().await { + if let Err(err) = wrap_message_and_forward_to_network::<_, O::Inner, C2, (), _>( + msg, + &network, + &user_id_mapping, + my_user_id, + associated_block_id, + associated_session_id, + associated_retry_id, + associated_task_id, + |m| MultiplexedChannelMessage::Channel1(m.inner_message()), + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); + } + } + }; + + let channel_2_task = async move { + while let Some(msg) = rx_to_outbound_2.next().await { + if let Err(err) = wrap_message_and_forward_to_network::<_, O::Inner, C2, (), _>( + msg, + &network_clone, + &user_id_mapping_clone, + my_user_id, + associated_block_id, + associated_session_id, + associated_retry_id, + associated_task_id, + |m| MultiplexedChannelMessage::Channel2(m), + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); + } + } + }; + + tokio::join!(channel_1_task, channel_2_task); + }); + + ( + tx_to_outbound_1, + rx_for_async_proto_1, + tx_to_outbound_2, + rx_for_async_proto_2, + ) +} + +pub type TriplexedChannel = ( + futures::channel::mpsc::UnboundedSender, + futures::channel::mpsc::UnboundedReceiver>, + futures::channel::mpsc::UnboundedSender, + futures::channel::mpsc::UnboundedReceiver>, + futures::channel::mpsc::UnboundedSender, + futures::channel::mpsc::UnboundedReceiver, +); + +#[allow(clippy::too_many_arguments)] +pub fn create_job_manager_to_async_protocol_channel_split_io_triplex< + N: Network + 'static, + C3: Serialize + DeserializeOwned + MaybeSenderReceiver + Send + 'static, + O1: InnerMessage + MaybeSenderReceiver + Send + 'static, + I1: InnerMessage + InnerMessageFromInbound + MaybeSenderReceiver + Send + 'static, + O2: InnerMessage + MaybeSenderReceiver + Send + 'static, + I2: InnerMessage + InnerMessageFromInbound + MaybeSenderReceiver + Send + 'static, +>( + mut rx_gadget: UnboundedReceiver, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + user_id_mapping: Arc>, + my_account_id: ecdsa::Public, + network: N, + logger: DebugLogger, +) -> TriplexedChannel { + let (tx_to_async_proto_1, rx_for_async_proto_1) = futures::channel::mpsc::unbounded(); + let (tx_to_async_proto_2, rx_for_async_proto_2) = futures::channel::mpsc::unbounded(); + let (tx_to_async_proto_3, rx_for_async_proto_3) = futures::channel::mpsc::unbounded(); + + let logger_outgoing = logger.clone(); + // Take the messages from the gadget and send them to the async protocol + tokio::task::spawn(async move { + let mut id = 0; + while let Some(msg_orig) = rx_gadget.recv().await { + if msg_orig.payload.is_empty() { + logger.warn(format!( + "Received empty message from Peer {}", + msg_orig.from + )); + continue; + } + match bincode2::deserialize::>( + &msg_orig.payload, + ) { + Ok(msg) => match msg { + MultiplexedChannelMessage::Channel1(msg) => { + let msg_type = if msg_orig.to.is_some() { + MessageType::P2P + } else { + MessageType::Broadcast + }; + + let incoming = + I1::from_inbound(id, msg_orig.from as PartyIndex, msg_type, msg); + + if tx_to_async_proto_1.unbounded_send(Ok(incoming)).is_err() { + logger.error("Failed to send Incoming message to protocol"); + } + + id += 1; + } + MultiplexedChannelMessage::Channel2(msg) => { + let msg_type = if msg_orig.to.is_some() { + MessageType::P2P + } else { + MessageType::Broadcast + }; + + let incoming = + I2::from_inbound(id, msg_orig.from as PartyIndex, msg_type, msg); + + if tx_to_async_proto_2.unbounded_send(Ok(incoming)).is_err() { + logger.error("Failed to send Incoming message to protocol"); + } + + id += 1; + } + MultiplexedChannelMessage::Channel3(msg) => { + if tx_to_async_proto_3.unbounded_send(msg).is_err() { + logger.error("Failed to send C2 message to protocol"); + } + } + }, + + Err(err) => { + logger.error(format!("Failed to deserialize message: {err:?}")); + } + } + } + }); + + let (tx_to_outbound_1, mut rx_to_outbound_1) = futures::channel::mpsc::unbounded::(); + let (tx_to_outbound_2, mut rx_to_outbound_2) = futures::channel::mpsc::unbounded::(); + let (tx_to_outbound_3, mut rx_to_outbound_3) = futures::channel::mpsc::unbounded::(); + + let my_user_id = user_id_mapping + .iter() + .find_map(|(user_id, account_id)| { + if *account_id == my_account_id { + Some(*user_id) + } else { + None + } + }) + .expect("Failed to find my user id"); + // Take the messages from the async protocol and send them to the gadget + tokio::task::spawn(async move { + let user_id_mapping = &user_id_mapping; + let network = &network; + let logger = &logger_outgoing; + let task0 = async move { + while let Some(msg) = rx_to_outbound_1.next().await { + if let Err(err) = + wrap_message_and_forward_to_network::<_, O1::Inner, O2::Inner, C3, _>( + msg, + network, + user_id_mapping, + my_user_id, + associated_block_id, + associated_session_id, + associated_retry_id, + associated_task_id, + |m| MultiplexedChannelMessage::Channel1(m.inner_message()), + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); + } + } + }; + + let task1 = async move { + while let Some(msg) = rx_to_outbound_2.next().await { + if let Err(err) = + wrap_message_and_forward_to_network::<_, O1::Inner, O2::Inner, C3, _>( + msg, + network, + user_id_mapping, + my_user_id, + associated_block_id, + associated_session_id, + associated_retry_id, + associated_task_id, + |m| MultiplexedChannelMessage::Channel2(m.inner_message()), + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); + } + } + }; + + let task2 = async move { + while let Some(msg) = rx_to_outbound_3.next().await { + if let Err(err) = + wrap_message_and_forward_to_network::<_, O1::Inner, O2::Inner, C3, _>( + msg, + network, + user_id_mapping, + my_user_id, + associated_block_id, + associated_session_id, + associated_retry_id, + associated_task_id, + |m| MultiplexedChannelMessage::Channel3(m), + logger, + ) + .await + { + logger.error(format!("Failed to send message to outbound: {err:?}")); + } + } + }; + + tokio::join!(task0, task1, task2); + }); + + ( + tx_to_outbound_1, + rx_for_async_proto_1, + tx_to_outbound_2, + rx_for_async_proto_2, + tx_to_outbound_3, + rx_for_async_proto_3, + ) +} + +#[allow(clippy::too_many_arguments)] +async fn wrap_message_and_forward_to_network< + N: Network, + C1: Serialize, + C2: Serialize, + C3: Serialize, + M, +>( + msg: M, + network: &N, + user_id_mapping: &HashMap, + my_user_id: UserID, + associated_block_id: ::Clock, + associated_session_id: ::SessionID, + associated_retry_id: ::RetryID, + associated_task_id: ::TaskID, + splitter: impl FnOnce(M) -> MultiplexedChannelMessage, + logger: &DebugLogger, +) -> Result<(), crate::Error> +where + M: MaybeSenderReceiver + Send + 'static, +{ + let from = msg.maybe_sender(); + let to = msg.maybe_receiver(); + logger.trace(format!("Sending message from {:?} to {:?}", from, to)); + let (to_account_id, from_account_id) = get_to_and_from_account_id( + user_id_mapping, + from.as_user_id().unwrap_or(my_user_id), + to.as_user_id(), + logger, + ); + + // let message_multiplexed = MultiplexedChannelMessage::::Channel1(msg.inner_message()); + let message_multiplexed = splitter(msg); + + let msg = GadgetProtocolMessage { + associated_block_id, + associated_session_id, + associated_retry_id, + task_hash: associated_task_id, + from: from.as_user_id().unwrap_or(my_user_id), + to: to.as_user_id(), + payload: bincode2::serialize(&message_multiplexed).expect("Failed to serialize message"), + from_network_id: from_account_id, + to_network_id: to_account_id, + }; + network.send_message(msg).await +} diff --git a/gadget-common/src/gadget/mod.rs b/gadget-common/src/gadget/mod.rs index 5aaf73c9b..b36949214 100644 --- a/gadget-common/src/gadget/mod.rs +++ b/gadget-common/src/gadget/mod.rs @@ -118,8 +118,7 @@ where let now: u64 = now_header.saturated_into(); *self.clock.write() = Some(now); self.protocol.logger().info(format!( - "[{}] Processing finality notification at block number {now}", - self.protocol.name() + "Processing finality notification at block number {now}", )); let jobs = self @@ -129,8 +128,7 @@ where .await?; self.protocol.logger().trace(format!( - "[{}] Found {} potential job(s) for initialization", - self.protocol.name(), + "Found {} potential job(s) for initialization", jobs.len() )); let mut relevant_jobs = Vec::new(); @@ -146,12 +144,12 @@ where } // Job is not for this role if !self.protocol.role_filter(job.job_type.get_role_type()) { - self.protocol.logger().trace(format!("[{}] The job {} requested for initialization is not for this role {:?}, skipping submission", self.protocol.name(), job.job_id, job.job_type.get_role_type())); + // self.protocol.logger().trace(format!("[{}] The job {} requested for initialization is not for this role {:?}, skipping submission", self.protocol.name(), job.job_id, job.job_type.get_role_type())); continue; } // Job is not for this phase if !self.protocol.phase_filter(job.job_type.clone()) { - self.protocol.logger().trace(format!("[{}] The job {} requested for initialization is not for this phase {:?}, skipping submission", self.protocol.name(), job.job_id, job.job_type)); + // self.protocol.logger().trace(format!("[{}] The job {} requested for initialization is not for this phase {:?}, skipping submission", self.protocol.name(), job.job_id, job.job_type)); continue; } @@ -208,6 +206,10 @@ where .await?; if let Some(role_key) = maybe_role_key { out.push(role_key); + } else { + self.protocol.logger().warn(format!( + "Participant {p} not found in the restaker registry", + )); } } out @@ -229,8 +231,7 @@ where let task_id = relevant_job.task_id; let retry_id = relevant_job.retry_id; self.protocol.logger().trace(format!( - "[{}] Creating job for task {task_id} with retry id {retry_id}", - self.protocol.name(), + "Creating job for task {task_id} with retry id {retry_id}", task_id = hex::encode(task_id), retry_id = retry_id )); @@ -271,7 +272,7 @@ where } Err(Error::ParticipantNotSelected { id, reason }) => { - self.protocol.logger().debug(format!("[{}] Participant {id} not selected for job {task_id} with retry id {retry_id} because {reason}", self.protocol.name(), id = id, task_id = hex::encode(task_id), retry_id = retry_id, reason = reason)); + self.protocol.logger().debug(format!("Participant {id} not selected for job {task_id} with retry id {retry_id} because {reason}", id = id, task_id = hex::encode(task_id), retry_id = retry_id, reason = reason)); } Err(err) => { @@ -361,7 +362,7 @@ where /// ## Example /// /// ```rust,ignore - /// fn phase_filter(&self, job: JobType) -> bool { + /// fn phase_filter(&self, job: JobType) -> bool { /// matches!(job, JobType::DKGTSSPhaseOne(_)) /// } /// ``` diff --git a/gadget-common/src/lib.rs b/gadget-common/src/lib.rs index 7472c5754..4104bbb24 100644 --- a/gadget-common/src/lib.rs +++ b/gadget-common/src/lib.rs @@ -24,6 +24,7 @@ pub use subxt_signer; use tokio::task::JoinError; pub use webb; +#[allow(ambiguous_glob_reexports)] pub mod prelude { pub use crate::client::*; pub use crate::config::*; @@ -42,6 +43,7 @@ pub mod prelude { pub use protocol_macros::protocol; pub use std::pin::Pin; pub use std::sync::Arc; + pub use tangle_primitives::jobs::*; pub use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; pub use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; } @@ -56,6 +58,8 @@ pub mod keystore; pub mod locks; pub mod prometheus; pub mod protocol; +pub mod utils; + #[derive(Debug)] pub enum Error { RegistryCreateError { err: String }, @@ -298,6 +302,9 @@ macro_rules! generate_protocol { key_store: ECDSAKeyStore, prometheus_config: $crate::prometheus::PrometheusConfig, ) -> Result { + let logger = DebugLogger { + peer_id: (logger.peer_id + " | " + stringify!($name)).replace("\"", "") + }; Ok(Self { pallet_tx, logger, diff --git a/gadget-common/src/utils.rs b/gadget-common/src/utils.rs new file mode 100644 index 000000000..acf9feb1a --- /dev/null +++ b/gadget-common/src/utils.rs @@ -0,0 +1,54 @@ +use futures::Stream; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use tokio::sync::mpsc::UnboundedReceiver; + +/// A Channel Receiver that can be cloned. +/// +/// On the second clone, the original channel will stop receiving new messages +/// and the new channel will start receiving any new messages after the clone. +pub struct CloneableUnboundedReceiver { + rx: Arc>>, + is_in_use: Arc, +} + +impl Clone for CloneableUnboundedReceiver { + fn clone(&self) -> Self { + // on the clone, we switch the is_in_use flag to false + // and we return a new channel + self.is_in_use + .store(false, std::sync::atomic::Ordering::SeqCst); + Self { + rx: self.rx.clone(), + is_in_use: Arc::new(AtomicBool::new(true)), + } + } +} + +impl From> for CloneableUnboundedReceiver { + fn from(rx: UnboundedReceiver) -> Self { + Self { + rx: Arc::new(tokio::sync::Mutex::new(rx)), + is_in_use: Arc::new(AtomicBool::new(false)), + } + } +} + +impl Stream for CloneableUnboundedReceiver { + type Item = T; + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + if !self.is_in_use.load(std::sync::atomic::Ordering::SeqCst) { + return std::task::Poll::Ready(None); + } + let mut rx = match self.rx.try_lock() { + Ok(rx) => rx, + Err(_) => return std::task::Poll::Pending, + }; + let rx = &mut *rx; + tokio::pin!(rx); + rx.poll_recv(cx) + } +} diff --git a/gadget-core/src/job_manager.rs b/gadget-core/src/job_manager.rs index 0e9c692f1..238d8acbf 100644 --- a/gadget-core/src/job_manager.rs +++ b/gadget-core/src/job_manager.rs @@ -668,7 +668,6 @@ mod tests { use crate::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; use parking_lot::Mutex; use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::Arc; use std::time::Duration; use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; diff --git a/protocols/bls/src/lib.rs b/protocols/bls/src/lib.rs index 4f6999831..02d98060c 100644 --- a/protocols/bls/src/lib.rs +++ b/protocols/bls/src/lib.rs @@ -1,15 +1,10 @@ use crate::protocol::keygen::BlsKeygenAdditionalParams; use async_trait::async_trait; use gadget_common::full_protocol::SharedOptional; -use gadget_common::gadget::JobInitMetadata; use gadget_common::prelude::*; -use gadget_common::{ - generate_protocol, generate_setup_and_run_command, BuiltExecutableJobWrapper, Error, JobError, - ProtocolWorkManager, WorkManagerInterface, -}; +use gadget_common::{generate_protocol, generate_setup_and_run_command}; use protocol::signing::BlsSigningAdditionalParams; use protocol_macros::protocol; -use std::sync::Arc; pub mod protocol; diff --git a/protocols/bls/src/protocol/keygen.rs b/protocols/bls/src/protocol/keygen.rs index 3a20b67b0..35f033e76 100644 --- a/protocols/bls/src/protocol/keygen.rs +++ b/protocols/bls/src/protocol/keygen.rs @@ -1,16 +1,11 @@ use crate::protocol::state_machine::payloads::RoundPayload; use crate::protocol::state_machine::BlsStateMachine; use gadget_common::gadget::message::UserID; -use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend}; use gadget_common::prelude::*; -use gadget_common::sp_core::{ecdsa, keccak_256, ByteArray}; +use gadget_common::sp_core::{ecdsa, keccak_256, ByteArray, Pair}; use itertools::Itertools; use round_based::Msg; use std::collections::{BTreeMap, HashMap}; -use tangle_primitives::jobs::{ - DKGTSSKeySubmissionResult, DigitalSignatureScheme, JobId, JobResult, -}; -use tokio::sync::mpsc::UnboundedSender; #[derive(Clone)] pub struct BlsKeygenAdditionalParams { @@ -92,6 +87,7 @@ where let job_id = additional_params.job_id; let pallet_tx = config.pallet_tx.clone(); let role_type = additional_params.role_type; + let id = keystore.pair().public(); let (i, t, n) = ( additional_params.i, additional_params.t, @@ -120,7 +116,9 @@ where associated_session_id, associated_task_id, user_id_to_account_id.clone(), + id, network.clone(), + logger.clone(), ); let me = round_based::AsyncProtocol::new(state_machine, rx0, tx0) diff --git a/protocols/bls/src/protocol/signing.rs b/protocols/bls/src/protocol/signing.rs index ab07a4c45..d1782d93a 100644 --- a/protocols/bls/src/protocol/signing.rs +++ b/protocols/bls/src/protocol/signing.rs @@ -6,7 +6,7 @@ use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; use gadget_common::gadget::work_manager::WorkManager; use gadget_common::gadget::JobInitMetadata; use gadget_common::keystore::KeystoreBackend; -use gadget_common::sp_core::{ecdsa, keccak_256}; +use gadget_common::sp_core::{ecdsa, keccak_256, Pair}; use gadget_common::{ Backend, Block, BuiltExecutableJobWrapper, Error, JobBuilder, JobError, ProtocolWorkManager, WorkManagerInterface, @@ -120,6 +120,7 @@ where let role_type = additional_params.role_type; let job_id = additional_params.job_id; let logger = config.logger.clone(); + let id = config.key_store.pair().public(); Ok(JobBuilder::new() .protocol(async move { @@ -136,7 +137,9 @@ where associated_session_id, associated_task_id, additional_params.user_id_to_account_id_mapping.clone(), + id, network, + logger.clone(), ); // Step 1: Generate shares diff --git a/protocols/dfns-cggmp21/Cargo.toml b/protocols/dfns-cggmp21/Cargo.toml index f714086cb..f91a3c865 100644 --- a/protocols/dfns-cggmp21/Cargo.toml +++ b/protocols/dfns-cggmp21/Cargo.toml @@ -11,13 +11,16 @@ protocol-macros = { workspace = true } async-trait = { workspace = true } log = { workspace = true } dfns-cggmp21 = { workspace = true, features = ["all-curves"] } +round-based-21 = { workspace = true } curv = { workspace = true } futures = { workspace = true } itertools = { workspace = true } bincode2 = { workspace = true } +digest = { workspace = true } pallet-jobs-rpc-runtime-api = { workspace = true, features = ["std"] } pallet-jobs = { workspace = true, features = ["std"] } +pallet-dkg = { workspace = true, features = ["std"] } tangle-primitives = { workspace = true, features = ["std"] } sp-core = { workspace = true, features = ["std"] } @@ -37,3 +40,4 @@ rand_chacha = { workspace = true } hex = { workspace = true } test-utils = { workspace = true } parking_lot = { workspace = true } +sha2 = { workspace = true } \ No newline at end of file diff --git a/protocols/dfns-cggmp21/src/lib.rs b/protocols/dfns-cggmp21/src/lib.rs index 65257c456..4dda5bd51 100644 --- a/protocols/dfns-cggmp21/src/lib.rs +++ b/protocols/dfns-cggmp21/src/lib.rs @@ -3,26 +3,11 @@ use crate::protocols::key_rotate::DfnsCGGMP21KeyRotateExtraParams; use crate::protocols::keygen::DfnsCGGMP21KeygenExtraParams; use crate::protocols::sign::DfnsCGGMP21SigningExtraParams; use async_trait::async_trait; -use gadget_common::client::{AccountId, ClientWithApi, JobsClient, PalletSubmitter}; -use gadget_common::client::{GadgetJobType, JobsApiForGadget}; -use gadget_common::debug_logger::DebugLogger; use gadget_common::full_protocol::SharedOptional; -use gadget_common::gadget::network::Network; -use gadget_common::gadget::JobInitMetadata; -use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend}; use gadget_common::prelude::*; -use gadget_common::prelude::{FullProtocolConfig, GadgetProtocolMessage, Mutex, WorkManager}; -use gadget_common::{generate_protocol, generate_setup_and_run_command, Error}; -use gadget_core::job::{BuiltExecutableJobWrapper, JobError}; -use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; +use gadget_common::{generate_protocol, generate_setup_and_run_command}; use protocol_macros::protocol; -use sc_client_api::Backend; -use sp_api::ProvideRuntimeApi; -use sp_runtime::traits::Block; -use std::sync::Arc; -use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; use test_utils::generate_signing_and_keygen_tss_tests; -use tokio::sync::mpsc::UnboundedReceiver; pub mod constants; pub mod error; @@ -73,4 +58,29 @@ generate_setup_and_run_command!( DfnsKeyRotateProtocol ); -generate_signing_and_keygen_tss_tests!(2, 3, 4, ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1); +mod secp256k1 { + super::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 4, + ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1 + ); +} + +mod secp256r1 { + super::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 4, + ThresholdSignatureRoleType::DfnsCGGMP21Secp256r1 + ); +} + +mod stark { + super::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 4, + ThresholdSignatureRoleType::DfnsCGGMP21Stark + ); +} diff --git a/protocols/dfns-cggmp21/src/protocols/key_refresh.rs b/protocols/dfns-cggmp21/src/protocols/key_refresh.rs index a04891aa7..1925ee3c2 100644 --- a/protocols/dfns-cggmp21/src/protocols/key_refresh.rs +++ b/protocols/dfns-cggmp21/src/protocols/key_refresh.rs @@ -1,16 +1,24 @@ -use dfns_cggmp21::supported_curves::Secp256k1; -use dfns_cggmp21::KeyShare; +use crate::protocols::{DefaultCryptoHasher, DefaultSecurityLevel}; +use dfns_cggmp21::generic_ec::Curve; +use dfns_cggmp21::key_refresh::msg::aux_only; +use dfns_cggmp21::key_refresh::AuxInfoGenerationBuilder; +use dfns_cggmp21::security_level::SecurityLevel; +use dfns_cggmp21::supported_curves::{Secp256k1, Secp256r1, Stark}; +use dfns_cggmp21::{KeyShare, PregeneratedPrimes}; +use digest::typenum::U32; +use digest::Digest; use gadget_common::client::{ClientWithApi, JobsApiForGadget}; use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; use gadget_common::gadget::network::Network; use gadget_common::gadget::work_manager::WorkManager; use gadget_common::gadget::JobInitMetadata; use gadget_common::keystore::KeystoreBackend; -use gadget_common::prelude::FullProtocolConfig; +use gadget_common::prelude::{DebugLogger, FullProtocolConfig}; use gadget_common::Block; use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; use rand::SeedableRng; +use round_based_21::{Incoming, Outgoing}; use sc_client_api::Backend; use sp_api::ProvideRuntimeApi; use sp_application_crypto::sp_core::keccak_256; @@ -20,7 +28,7 @@ use std::sync::Arc; use tangle_primitives::jobs::{ DKGTSSKeyRefreshResult, DigitalSignatureScheme, JobId, JobResult, JobType, }; -use tangle_primitives::roles::RoleType; +use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; use tokio::sync::mpsc::UnboundedReceiver; pub(crate) async fn create_next_job< @@ -96,7 +104,7 @@ pub struct DfnsCGGMP21KeyRefreshExtraParams { job_id: JobId, phase_one_id: JobId, role_type: RoleType, - key: KeyShare, + key: Vec, pregenerated_primes: dfns_cggmp21::PregeneratedPrimes, user_id_to_account_id_mapping: Arc>, } @@ -126,70 +134,83 @@ where let role_id = config.key_store.pair().public(); let logger = config.logger.clone(); let network = config.clone(); + let role_type = additional_params.role_type; - let (mapping, key, pregenerated_primes) = ( + let (mapping, serialized_key_share, pregenerated_primes) = ( additional_params.user_id_to_account_id_mapping, additional_params.key, additional_params.pregenerated_primes, ); - let i = key.i; - let n = key.public_shares.len() as u16; - let t = key.vss_setup.as_ref().map(|x| x.min_signers).unwrap_or(n); Ok(JobBuilder::new() .protocol(async move { - let mut rng = rand::rngs::StdRng::from_entropy(); - let protocol_message_channel = - super::util::CloneableUnboundedReceiver::from(protocol_message_channel); - logger.info(format!( - "Starting KeyRefresh Protocol with params: i={i}, t={t}, n={n}" - )); - - let job_id_bytes = additional_params.job_id.to_be_bytes(); - let mix = keccak_256(b"dnfs-cggmp21-keyrefresh"); - let eid_bytes = [&job_id_bytes[..], &mix[..]].concat(); - let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes); - - let ( - key_refresh_tx_to_outbound, - key_refresh_rx_async_proto, - _broadcast_tx_to_outbound, - _broadcast_rx_from_gadget, - ) = super::util::create_job_manager_to_async_protocol_channel_split::<_, (), _>( - protocol_message_channel.clone(), - associated_block_id, - associated_retry_id, - associated_session_id, - associated_task_id, - mapping.clone(), - role_id, - network.clone(), - ); - - let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); - let delivery = (key_refresh_rx_async_proto, key_refresh_tx_to_outbound); - let party = dfns_cggmp21::round_based::MpcParty::connected(delivery); - let aux_info = dfns_cggmp21::aux_info_gen(eid, i, n, pregenerated_primes) - .set_progress_tracer(&mut tracer) - .start(&mut rng, party) - .await - .map_err(|err| JobError { - reason: format!("KeyRefresh protocol error: {err:?}"), - })?; - - let key = key.update_aux(aux_info).map_err(|err| JobError { - reason: format!("KeyRefresh protocol error: {err:?}"), - })?; - let perf_report = tracer.get_report().map_err(|err| JobError { - reason: format!("KeyRefresh protocol error: {err:?}"), - })?; - logger.trace(format!("KeyRefresh protocol report: {perf_report}")); - - logger.debug("Finished AsyncProtocol - KeyRefresh"); + let key = match role_type { + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1) => { + handle_key_refresh::( + &serialized_key_share, + &logger, + additional_params.job_id, + pregenerated_primes, + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping, + role_id, + network, + ) + .await? + } + + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256r1) => { + handle_key_refresh::( + &serialized_key_share, + &logger, + additional_params.job_id, + pregenerated_primes, + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping, + role_id, + network, + ) + .await? + } + + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Stark) => { + handle_key_refresh::( + &serialized_key_share, + &logger, + additional_params.job_id, + pregenerated_primes, + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping, + role_id, + network, + ) + .await? + } + _ => { + return Err(JobError { + reason: format!( + "Role type {role_type:?} is not supported for KeyRefresh protocol" + ), + }) + } + }; let job_result = JobResult::DKGPhaseThree(DKGTSSKeyRefreshResult { signature_scheme: DigitalSignatureScheme::Ecdsa, }); + *protocol_output.lock().await = Some((key, job_result)); Ok(()) }) @@ -220,3 +241,97 @@ where }) .build()) } + +#[allow(clippy::too_many_arguments)] +async fn handle_key_refresh< + E: Curve, + S: SecurityLevel, + H: Digest + Clone + Send + 'static, + N: Network, +>( + serialized_key_share: &[u8], + logger: &DebugLogger, + job_id: JobId, + pregenerated_primes: PregeneratedPrimes, + protocol_message_channel: UnboundedReceiver, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + mapping: Arc>, + role_id: ecdsa::Public, + network: N, +) -> Result, JobError> { + let mut rng = rand::rngs::StdRng::from_entropy(); + let local_key_share: KeyShare = + super::sign::get_local_key_share_from_serialized_local_key_bytes::( + serialized_key_share, + )?; + let i = local_key_share.i; + let n = local_key_share.public_shares.len() as u16; + let t = local_key_share + .vss_setup + .as_ref() + .map(|x| x.min_signers) + .unwrap_or(n); + + logger.info(format!( + "Starting KeyRefresh Protocol with params: i={i}, t={t}, n={n}" + )); + + let job_id_bytes = job_id.to_be_bytes(); + let mix = keccak_256(b"dnfs-cggmp21-keyrefresh"); + let eid_bytes = [&job_id_bytes[..], &mix[..]].concat(); + let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes); + + let ( + key_refresh_tx_to_outbound, + key_refresh_rx_async_proto, + _broadcast_tx_to_outbound, + _broadcast_rx_from_gadget, + ) = gadget_common::channels::create_job_manager_to_async_protocol_channel_split_io::< + _, + (), + Outgoing>, + Incoming>, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + role_id, + network.clone(), + logger.clone(), + ); + + let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); + let delivery = (key_refresh_rx_async_proto, key_refresh_tx_to_outbound); + let party = round_based_21::MpcParty::connected(delivery); + let aux_info_builder = + AuxInfoGenerationBuilder::::new_aux_gen(eid, i, n, pregenerated_primes); + let aux_info = aux_info_builder + .set_progress_tracer(&mut tracer) + .start(&mut rng, party) + .await + .map_err(|err| JobError { + reason: format!("KeyRefresh protocol error: {err:?}"), + })?; + + let key = local_key_share + .update_aux(aux_info) + .map_err(|err| JobError { + reason: format!("KeyRefresh protocol error: {err:?}"), + })?; + let perf_report = tracer.get_report().map_err(|err| JobError { + reason: format!("KeyRefresh protocol error: {err:?}"), + })?; + logger.trace(format!("KeyRefresh protocol report: {perf_report}")); + + logger.debug("Finished AsyncProtocol - KeyRefresh"); + let serialized_local_key = bincode2::serialize(&key).map_err(|err| JobError { + reason: format!("KeyRefresh protocol error: {err:?}"), + })?; + Ok(serialized_local_key) +} diff --git a/protocols/dfns-cggmp21/src/protocols/key_rotate.rs b/protocols/dfns-cggmp21/src/protocols/key_rotate.rs index 2bd638d58..46c8d0488 100644 --- a/protocols/dfns-cggmp21/src/protocols/key_rotate.rs +++ b/protocols/dfns-cggmp21/src/protocols/key_rotate.rs @@ -1,5 +1,7 @@ -use dfns_cggmp21::supported_curves::Secp256k1; -use dfns_cggmp21::KeyShare; +use crate::protocols::sign::run_and_serialize_signing; +use dfns_cggmp21::signing::msg::Msg; +use dfns_cggmp21::supported_curves::{Secp256k1, Secp256r1, Stark}; + use gadget_common::client::{ClientWithApi, JobsApiForGadget}; use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; use gadget_common::gadget::network::Network; @@ -7,10 +9,13 @@ use gadget_common::gadget::work_manager::WorkManager; use gadget_common::gadget::JobInitMetadata; use gadget_common::keystore::KeystoreBackend; use gadget_common::prelude::FullProtocolConfig; +use gadget_common::prelude::*; use gadget_common::Block; use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; use rand::SeedableRng; + +use crate::protocols::{DefaultCryptoHasher, DefaultSecurityLevel}; use sc_client_api::Backend; use sp_api::ProvideRuntimeApi; use sp_core::{ecdsa, keccak_256, Pair}; @@ -22,6 +27,8 @@ use tangle_primitives::jobs::{ use tangle_primitives::roles::RoleType; use tokio::sync::mpsc::UnboundedReceiver; +use super::keygen::create_party; + pub async fn create_next_job< B: Block, BE: Backend + 'static, @@ -49,31 +56,29 @@ where let seed = keccak_256(&[&job_id.to_be_bytes()[..], &job.retry_id.to_be_bytes()[..]].concat()); let mut rng = rand_chacha::ChaChaRng::from_seed(seed); + let my_id = config.key_store.pair().public(); - let (i, signers, mapping) = super::util::choose_signers( - &mut rng, - &config.key_store.pair().public(), - &job.participants_role_ids, - t, - )?; + let (i, signers, mapping) = + super::util::choose_signers(&mut rng, &my_id, &job.participants_role_ids, t)?; + config.logger.info(format!( + "We are selected to sign: i={i}, signers={signers:?} | signers len: {}", + signers.len() + )); + config + .logger + .info(format!("Mapping for network: {mapping:?}")); - let new_phase_one_result = config - .get_jobs_client() - .query_job_result(job.at, job.role_type, new_phase_one_id) - .await? - .ok_or_else(|| gadget_common::Error::ClientError { - err: format!("No key found for job ID: {new_phase_one_id}"), + let new_key = config + .key_store + .get_job_result(new_phase_one_id) + .await + .map_err(|err| Error::ClientError { + err: err.to_string(), + })? + .ok_or_else(|| Error::ClientError { + err: format!("No new key found for job ID: {new_phase_one_id:?}"), })?; - let new_key = match new_phase_one_result.result { - JobResult::DKGPhaseOne(r) => r.key, - _ => { - return Err(gadget_common::Error::ClientError { - err: format!("Wrong job result type for job ID: {new_phase_one_id}"), - }) - } - }; - let key = config .key_store .get_job_result(phase_one_id) @@ -85,8 +90,12 @@ where err: format!("No key found for job ID: {job_id:?}"), })?; + config.logger.info("RB4"); + let user_id_to_account_id_mapping = Arc::new(mapping); + config.logger.info("RB5"); + let params = DfnsCGGMP21KeyRotateExtraParams { i, t, @@ -96,9 +105,10 @@ where new_phase_one_id, role_type: job.role_type, key, - new_key: new_key.try_into().unwrap(), + new_key, user_id_to_account_id_mapping, }; + Ok(params) } @@ -111,7 +121,7 @@ pub struct DfnsCGGMP21KeyRotateExtraParams { phase_one_id: JobId, new_phase_one_id: JobId, role_type: RoleType, - key: KeyShare, + key: Vec, new_key: Vec, user_id_to_account_id_mapping: Arc>, } @@ -143,24 +153,40 @@ where let phase_one_id = additional_params.phase_one_id; let network = config.clone(); - let (i, signers, t, new_phase_one_id, key, new_key, mapping) = ( + let ( + i, + signers, + t, + new_phase_one_id, + serialized_key_share, + role_type, + new_serialized_key_share, + mapping, + ) = ( additional_params.i, additional_params.signers, additional_params.t, additional_params.new_phase_one_id, additional_params.key, + additional_params.role_type, additional_params.new_key.clone(), additional_params.user_id_to_account_id_mapping.clone(), ); - let public_key_bytes = key.shared_public_key().to_bytes(true).to_vec(); - let new_key2 = new_key.clone(); + let public_key = super::sign::get_public_key_from_serialized_key_share_bytes::< + DefaultSecurityLevel, + >(&role_type, &serialized_key_share)?; + + let new_public_key = super::sign::get_public_key_from_serialized_key_share_bytes::< + DefaultSecurityLevel, + >(&role_type, &new_serialized_key_share)?; + // We're signing over the hash of the new public key using the old public key + let data_hash = keccak_256(&new_public_key); + let data_to_sign_bytes = data_hash.to_vec(); Ok(JobBuilder::new() .protocol(async move { let mut rng = rand::rngs::StdRng::from_entropy(); - let protocol_message_channel = - super::util::CloneableUnboundedReceiver::from(protocol_message_channel); logger.info(format!( "Starting Key Rotation Protocol with params: i={i}, t={t}" @@ -170,37 +196,131 @@ where let mix = keccak_256(b"dnfs-cggmp21-key-rotate"); let eid_bytes = [&job_id_bytes[..], &mix[..]].concat(); let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes); - let ( - key_rotate_tx_to_outbound, - key_rotate_rx_async_proto, - _broadcast_tx_to_outbound, - _broadcast_rx_from_gadget, - ) = super::util::create_job_manager_to_async_protocol_channel_split::<_, (), _>( - protocol_message_channel.clone(), - associated_block_id, - associated_retry_id, - associated_session_id, - associated_task_id, - mapping.clone(), - role_id, - network.clone(), - ); - - let delivery = (key_rotate_rx_async_proto, key_rotate_tx_to_outbound); - let party = dfns_cggmp21::round_based::MpcParty::connected(delivery); - let data_hash = keccak_256(&new_key); - let data_to_sign = dfns_cggmp21::DataToSign::from_scalar( - dfns_cggmp21::generic_ec::Scalar::from_be_bytes_mod_order(data_hash), - ); - let signature = dfns_cggmp21::signing(eid, i, &signers, &key) - .sign(&mut rng, party, data_to_sign) - .await - .map_err(|err| JobError { - reason: format!("Key Rotation protocol error: {err:?}"), - })?; - - // Normalize the signature - let signature = signature.normalize_s(); + let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); + let signature = match role_type { + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1) => { + let data_to_sign = dfns_cggmp21::DataToSign::::from_scalar( + dfns_cggmp21::generic_ec::Scalar::::from_be_bytes_mod_order( + data_hash, + ), + ); + let (_, _, party) = create_party::< + Secp256k1, + _, + DefaultSecurityLevel, + Msg, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + role_id, + network.clone(), + logger.clone(), + ); + run_and_serialize_signing::<_, DefaultSecurityLevel, _, _, DefaultCryptoHasher>( + &logger, + &mut tracer, + eid, + i, + signers, + data_to_sign, + serialized_key_share, + party, + &mut rng, + ) + .await? + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256r1) => { + let data_to_sign = dfns_cggmp21::DataToSign::::from_scalar( + dfns_cggmp21::generic_ec::Scalar::::from_be_bytes_mod_order( + data_hash, + ), + ); + let (_, _, party) = create_party::< + Secp256r1, + _, + DefaultSecurityLevel, + Msg, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + role_id, + network.clone(), + logger.clone(), + ); + run_and_serialize_signing::< + Secp256r1, + DefaultSecurityLevel, + _, + _, + DefaultCryptoHasher, + >( + &logger, + &mut tracer, + eid, + i, + signers, + data_to_sign, + serialized_key_share, + party, + &mut rng, + ) + .await? + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Stark) => { + let data_to_sign = dfns_cggmp21::DataToSign::::from_scalar( + dfns_cggmp21::generic_ec::Scalar::::from_be_bytes_mod_order( + data_hash, + ), + ); + let (_, _, party) = create_party::< + Stark, + _, + DefaultSecurityLevel, + Msg, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + role_id, + network.clone(), + logger.clone(), + ); + run_and_serialize_signing::< + Stark, + DefaultSecurityLevel, + _, + _, + DefaultCryptoHasher, + >( + &logger, + &mut tracer, + eid, + i, + signers, + data_to_sign, + serialized_key_share, + party, + &mut rng, + ) + .await? + } + _ => { + return Err(JobError { + reason: format!("Unsupported role type: {role_type:?}"), + }); + } + }; logger.debug("Finished AsyncProtocol - Key Rotation"); *protocol_output.lock().await = Some(signature); Ok(()) @@ -208,50 +328,18 @@ where .post(async move { // Submit the protocol output to the blockchain if let Some(signature) = protocol_output_clone.lock().await.take() { - let mut signature_bytes = [0u8; 65]; - signature.write_to_slice(&mut signature_bytes[0..64]); - // To figure out the recovery ID, we need to try all possible values of v - // in our case, v can be 0 or 1 - let mut v = 0u8; - loop { - let mut signature_bytes = signature_bytes; - let data_hash = keccak_256(&new_key2); - signature_bytes[64] = v; - let res = sp_io::crypto::secp256k1_ecdsa_recover(&signature_bytes, &data_hash); - match res { - Ok(key) if key[..32] == public_key_bytes[1..] => { - // Found the correct v - break; - } - Ok(_) => { - // Found a key, but not the correct one - // Try the other v value - v = 1; - continue; - } - Err(_) if v == 1 => { - // We tried both v values, but no key was found - // This should never happen, but if it does, we will just - // leave v as 1 and break - break; - } - Err(_) => { - // No key was found, try the other v value - v = 1; - continue; - } - } - } - // Add 27 to the recovery ID - signature_bytes[64] = v + 27; - + let signature = super::sign::convert_dfns_signature( + signature, + &data_to_sign_bytes, + &new_public_key, + ); let job_result = JobResult::DKGPhaseFour(DKGTSSKeyRotationResult { signature_scheme: DigitalSignatureScheme::Ecdsa, - signature: signature_bytes.to_vec().try_into().unwrap(), + signature: signature.to_vec().try_into().unwrap(), phase_one_id, new_phase_one_id, - new_key: new_key2.try_into().unwrap(), - key: public_key_bytes.try_into().unwrap(), + new_key: new_public_key.try_into().unwrap(), + key: public_key.try_into().unwrap(), }); client diff --git a/protocols/dfns-cggmp21/src/protocols/keygen.rs b/protocols/dfns-cggmp21/src/protocols/keygen.rs index 13de4f92b..87d31e425 100644 --- a/protocols/dfns-cggmp21/src/protocols/keygen.rs +++ b/protocols/dfns-cggmp21/src/protocols/keygen.rs @@ -1,5 +1,16 @@ -use dfns_cggmp21::supported_curves::Secp256k1; -use dfns_cggmp21::KeyShare; +use dfns_cggmp21::generic_ec::Curve; +use dfns_cggmp21::key_refresh::msg::aux_only; + +use dfns_cggmp21::key_refresh::AuxInfoGenerationBuilder; +use dfns_cggmp21::keygen::msg::threshold::Msg; +use dfns_cggmp21::keygen::KeygenBuilder; +use dfns_cggmp21::progress::PerfProfiler; +use dfns_cggmp21::security_level::SecurityLevel; +use dfns_cggmp21::supported_curves::{Secp256k1, Secp256r1, Stark}; +use dfns_cggmp21::PregeneratedPrimes; +use digest::typenum::U32; +use digest::Digest; +use futures::channel::mpsc::{TryRecvError, UnboundedSender}; use futures::StreamExt; use gadget_common::client::{ ClientWithApi, GadgetJobResult, JobsApiForGadget, MaxKeyLen, MaxParticipants, MaxSignatureLen, @@ -15,20 +26,27 @@ use gadget_common::Block; use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; use itertools::Itertools; -use rand::SeedableRng; +use pallet_dkg::signatures_schemes::ecdsa::verify_signer_from_set_ecdsa; +use pallet_dkg::signatures_schemes::to_slice_33; +use rand::rngs::{OsRng, StdRng}; +use rand::{CryptoRng, RngCore, SeedableRng}; +use round_based_21::{Delivery, Incoming, MpcParty, Outgoing}; use sc_client_api::Backend; +use serde::Serialize; use sp_api::ProvideRuntimeApi; use sp_application_crypto::sp_core::keccak_256; use sp_core::{ecdsa, Pair}; +use sp_runtime::DeserializeOwned; use std::collections::{BTreeMap, HashMap}; use std::sync::Arc; use tangle_primitives::jobs::{ DKGTSSKeySubmissionResult, DigitalSignatureScheme, JobId, JobResult, JobType, }; -use tangle_primitives::roles::RoleType; +use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; use tokio::sync::mpsc::UnboundedReceiver; -use super::util::PublicKeyGossipMessage; +use crate::protocols::{DefaultCryptoHasher, DefaultSecurityLevel}; +use gadget_common::channels::PublicKeyGossipMessage; pub async fn create_next_job< B: Block, @@ -92,6 +110,152 @@ pub struct DfnsCGGMP21KeygenExtraParams { user_id_to_account_id_mapping: Arc>, } +pub async fn run_and_serialize_keygen< + 'r, + E: Curve, + S: SecurityLevel, + H: Digest + Clone + Send + 'static, + D, + R, +>( + tracer: &mut PerfProfiler, + eid: dfns_cggmp21::ExecutionId<'r>, + i: u16, + n: u16, + t: u16, + party: MpcParty, D>, + mut rng: R, +) -> Result, JobError> +where + D: Delivery>, + R: RngCore + CryptoRng, +{ + let builder = KeygenBuilder::::new(eid, i, n); + let incomplete_key_share = builder + .set_progress_tracer(tracer) + .set_threshold(t) + .start(&mut rng, party) + .await + .map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + })?; + bincode2::serialize(&incomplete_key_share).map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + }) +} + +#[allow(clippy::too_many_arguments)] +pub async fn run_and_serialize_keyrefresh< + 'r, + E: Curve, + S: SecurityLevel, + H: Digest + Clone + Send + 'static, + D, +>( + logger: &DebugLogger, + incomplete_key_share: Vec, + pregenerated_primes: PregeneratedPrimes, + tracer: &mut PerfProfiler, + aux_eid: dfns_cggmp21::ExecutionId<'r>, + i: u16, + n: u16, + party: MpcParty, D>, + mut rng: StdRng, +) -> Result<(Vec, Vec), JobError> +where + D: Delivery>, +{ + let incomplete_key_share: dfns_cggmp21::key_share::Valid< + dfns_cggmp21::key_share::DirtyIncompleteKeyShare<_>, + > = bincode2::deserialize(&incomplete_key_share).map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + })?; + + let aux_info_builder = + AuxInfoGenerationBuilder::::new_aux_gen(aux_eid, i, n, pregenerated_primes); + + let aux_info = aux_info_builder + .set_progress_tracer(tracer) + .start(&mut rng, party) + .await + .map_err(|err| JobError { + reason: format!("Aux info protocol error: {err:?}"), + })?; + let perf_report = tracer.get_report().map_err(|err| JobError { + reason: format!("Aux info protocol error: {err:?}"), + })?; + logger.trace(format!("Aux info protocol report: {perf_report}")); + logger.debug("Finished AsyncProtocol - Aux Info"); + + let key_share = + dfns_cggmp21::KeyShare::::make(incomplete_key_share, aux_info).map_err(|err| { + JobError { + reason: format!("Key share error: {err:?}"), + } + })?; + // Serialize the key share and the public key + bincode2::serialize(&key_share) + .map(|ks| (ks, key_share.shared_public_key().to_bytes(true).to_vec())) + .map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + }) +} + +pub type CreatePartyResult = ( + UnboundedSender, + futures::channel::mpsc::UnboundedReceiver, + MpcParty< + M, + ( + futures::channel::mpsc::UnboundedReceiver, TryRecvError>>, + UnboundedSender>, + ), + >, +); + +#[allow(clippy::too_many_arguments)] +pub fn create_party( + protocol_message_channel: UnboundedReceiver, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + mapping: Arc>, + id: ecdsa::Public, + network: N, + logger: DebugLogger, +) -> CreatePartyResult +where + N: Network, + L: SecurityLevel, + E: Curve, + M: Serialize + DeserializeOwned + Send + Sync + 'static, +{ + let (tx_to_outbound, rx_async_proto, broadcast_tx_to_outbound, broadcast_rx_from_gadget) = + gadget_common::channels::create_job_manager_to_async_protocol_channel_split_io::< + _, + PublicKeyGossipMessage, + Outgoing, + Incoming, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping, + id, + network, + logger, + ); + let delivery = (rx_async_proto, tx_to_outbound); + ( + broadcast_tx_to_outbound, + broadcast_rx_from_gadget, + MpcParty::connected(delivery), + ) +} + pub async fn generate_protocol_from< B: Block, BE: Backend + 'static, @@ -119,18 +283,17 @@ where let logger = config.logger.clone(); let network = config.clone(); - let (i, t, n, mapping) = ( + let (i, t, n, role_type, mapping) = ( additional_params.i, additional_params.t, additional_params.n, + additional_params.role_type, additional_params.user_id_to_account_id_mapping, ); Ok(JobBuilder::new() .protocol(async move { - let mut rng = rand::rngs::StdRng::from_entropy(); - let protocol_message_channel = - super::util::CloneableUnboundedReceiver::from(protocol_message_channel); + let rng = rand::rngs::StdRng::from_entropy(); logger.info(format!( "Starting Keygen Protocol with params: i={i}, t={t}, n={n}" )); @@ -142,98 +305,114 @@ where let mix = keccak_256(b"dnfs-cggmp21-keygen-aux"); let aux_eid_bytes = [&job_id_bytes[..], &mix[..]].concat(); let aux_eid = dfns_cggmp21::ExecutionId::new(&aux_eid_bytes); - - let ( - keygen_tx_to_outbound, - keygen_rx_async_proto, - _broadcast_tx_to_outbound, - _broadcast_rx_from_gadget, - ) = super::util::create_job_manager_to_async_protocol_channel_split::<_, (), _>( - protocol_message_channel.clone(), - associated_block_id, - associated_retry_id, - associated_session_id, - associated_task_id, - mapping.clone(), - my_role_id, - network.clone(), - ); - let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); - let delivery = (keygen_rx_async_proto, keygen_tx_to_outbound); - let party = dfns_cggmp21::round_based::MpcParty::connected(delivery); - let incomplete_key_share = dfns_cggmp21::keygen::(eid, i, n) - .set_progress_tracer(&mut tracer) - .set_threshold(t) - .start(&mut rng, party) - .await - .map_err(|err| JobError { - reason: format!("Keygen protocol error: {err:?}"), - })?; - - let perf_report = tracer.get_report().map_err(|err| JobError { - reason: format!("Keygen protocol error: {err:?}"), - })?; - logger.trace(format!("Incomplete Keygen protocol report: {perf_report}")); - logger.debug("Finished AsyncProtocol - Incomplete Keygen"); - - let ( - keygen_tx_to_outbound, - keygen_rx_async_proto, - broadcast_tx_to_outbound, - broadcast_rx_from_gadget, - ) = super::util::create_job_manager_to_async_protocol_channel_split( - protocol_message_channel.clone(), - associated_block_id, - associated_retry_id, - associated_session_id, - associated_task_id, - mapping, - my_role_id, - network, - ); - let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); - - let pregenerated_primes_key = - keccak_256(&[&b"dfns-cggmp21-keygen-primes"[..], &job_id_bytes[..]].concat()); - let pregenerated_primes = dfns_cggmp21::PregeneratedPrimes::generate(&mut rng); - key_store2 - .set(&pregenerated_primes_key, pregenerated_primes.clone()) - .await - .map_err(|err| JobError { - reason: format!("Failed to store pregenerated primes: {err:?}"), - })?; - let delivery = (keygen_rx_async_proto, keygen_tx_to_outbound); - let party = dfns_cggmp21::round_based::MpcParty::connected(delivery); - let aux_info = dfns_cggmp21::aux_info_gen(aux_eid, i, n, pregenerated_primes) - .set_progress_tracer(&mut tracer) - .start(&mut rng, party) - .await - .map_err(|err| JobError { - reason: format!("Aux info protocol error: {err:?}"), - })?; - let perf_report = tracer.get_report().map_err(|err| JobError { - reason: format!("Aux info protocol error: {err:?}"), - })?; - logger.trace(format!("Aux info protocol report: {perf_report}")); - logger.debug("Finished AsyncProtocol - Aux Info"); - - let key_share = - dfns_cggmp21::KeyShare::make(incomplete_key_share, aux_info).map_err(|err| { - JobError { - reason: format!("Key share error: {err:?}"), + let tracer = PerfProfiler::new(); + + let (key_share, serialized_public_key, tx2, rx2) = + match role_type { + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1) => { + run_full_keygen_protocol::< + Secp256k1, + DefaultSecurityLevel, + DefaultCryptoHasher, + _, + _, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + my_role_id, + network.clone(), + tracer, + eid, + aux_eid, + i, + n, + t, + rng, + &logger, + &job_id_bytes, + &key_store, + role_type, + ) + .await? } - })?; + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256r1) => { + run_full_keygen_protocol::< + Secp256r1, + DefaultSecurityLevel, + DefaultCryptoHasher, + _, + _, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + my_role_id, + network.clone(), + tracer, + eid, + aux_eid, + i, + n, + t, + rng, + &logger, + &job_id_bytes, + &key_store, + role_type, + ) + .await? + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Stark) => { + run_full_keygen_protocol::< + Stark, + DefaultSecurityLevel, + DefaultCryptoHasher, + _, + _, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + my_role_id, + network.clone(), + tracer, + eid, + aux_eid, + i, + n, + t, + rng, + &logger, + &job_id_bytes, + &key_store, + role_type, + ) + .await? + } + _ => unreachable!("Invalid role type"), + }; logger.debug("Finished AsyncProtocol - Keygen"); let job_result = handle_public_key_gossip( - key_store2, + key_store, &logger, &key_share, + &serialized_public_key, t, i, - broadcast_tx_to_outbound, - broadcast_rx_from_gadget, + tx2, + rx2, ) .await?; @@ -244,7 +423,7 @@ where // TODO: handle protocol blames // Store the keys locally, as well as submitting them to the blockchain if let Some((local_key, job_result)) = protocol_output_clone.lock().await.take() { - key_store + key_store2 .set_job_result(additional_params.job_id, local_key) .await .map_err(|err| JobError { @@ -272,14 +451,14 @@ where async fn handle_public_key_gossip( key_store: ECDSAKeyStore, logger: &DebugLogger, - local_key: &KeyShare, + _local_key: &[u8], + serialized_public_key: &[u8], t: u16, i: u16, - broadcast_tx_to_outbound: futures::channel::mpsc::UnboundedSender, + broadcast_tx_to_outbound: UnboundedSender, mut broadcast_rx_from_gadget: futures::channel::mpsc::UnboundedReceiver, ) -> Result { - let serialized_public_key = local_key.shared_public_key().to_bytes(true).to_vec(); - let key_hashed = keccak_256(&serialized_public_key); + let key_hashed = keccak_256(serialized_public_key); let signature = key_store.pair().sign_prehashed(&key_hashed).0.to_vec(); let my_id = key_store.pair().public(); let mut received_keys = BTreeMap::new(); @@ -314,7 +493,7 @@ async fn handle_public_key_gossip( continue; } // verify signature - let maybe_signature = sp_core::ecdsa::Signature::from_slice(&message.signature); + let maybe_signature = ecdsa::Signature::from_slice(&message.signature); match maybe_signature.and_then(|s| s.recover_prehashed(&key_hashed)) { Some(p) if p != message.id => { logger.warn(format!( @@ -367,7 +546,7 @@ async fn handle_public_key_gossip( let res = DKGTSSKeySubmissionResult { signature_scheme: DigitalSignatureScheme::Ecdsa, - key: serialized_public_key.try_into().unwrap(), + key: serialized_public_key.to_vec().try_into().unwrap(), participants, signatures: signatures.try_into().unwrap(), threshold: t as _, @@ -428,51 +607,116 @@ fn verify_generated_dkg_key_ecdsa( )); } -pub fn verify_signer_from_set_ecdsa( - maybe_signers: Vec, - msg: &[u8], - signature: &[u8], -) -> (Option, bool) { - let mut signer = None; - let res = maybe_signers.iter().any(|x| { - if let Some(data) = recover_ecdsa_pub_key(msg, signature) { - let recovered = &data[..32]; - if x.0[1..].to_vec() == recovered.to_vec() { - signer = Some(*x); - true - } else { - false - } - } else { - false - } - }); - - (signer, res) -} - -pub fn recover_ecdsa_pub_key(data: &[u8], signature: &[u8]) -> Option> { - const SIGNATURE_LENGTH: usize = 65; - if signature.len() != SIGNATURE_LENGTH { - return None; - } - let mut sig = [0u8; SIGNATURE_LENGTH]; - sig[..SIGNATURE_LENGTH].copy_from_slice(signature); - - let hash = keccak_256(data); - - sp_io::crypto::secp256k1_ecdsa_recover(&sig, &hash) - .ok() - .map(|x| x.to_vec()) +async fn handle_post_incomplete_keygen( + tracer: &PerfProfiler, + logger: &DebugLogger, + job_id_bytes: &[u8], + key_store: &ECDSAKeyStore, +) -> Result<(PerfProfiler, PregeneratedPrimes), JobError> { + let perf_report = tracer.get_report().map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + })?; + logger.trace(format!("Incomplete Keygen protocol report: {perf_report}")); + logger.debug("Finished AsyncProtocol - Incomplete Keygen"); + let tracer = PerfProfiler::new(); + + let pregenerated_primes_key = + keccak_256(&[&b"dfns-cggmp21-keygen-primes"[..], job_id_bytes].concat()); + let now = tokio::time::Instant::now(); + let pregenerated_primes = tokio::task::spawn_blocking(|| { + let mut rng = OsRng; + dfns_cggmp21::PregeneratedPrimes::::generate(&mut rng) + }) + .await + .map_err(|err| JobError { + reason: format!("Failed to generate pregenerated primes: {err:?}"), + })?; + + let elapsed = now.elapsed(); + logger.debug(format!("Pregenerated primes took {elapsed:?}")); + + key_store + .set(&pregenerated_primes_key, pregenerated_primes.clone()) + .await + .map_err(|err| JobError { + reason: format!("Failed to store pregenerated primes: {err:?}"), + })?; + Ok((tracer, pregenerated_primes)) } -pub fn to_slice_33(val: &[u8]) -> Option<[u8; 33]> { - const ECDSA_KEY_LENGTH: usize = 33; - if val.len() == ECDSA_KEY_LENGTH { - let mut key = [0u8; ECDSA_KEY_LENGTH]; - key[..ECDSA_KEY_LENGTH].copy_from_slice(val); +#[allow(clippy::too_many_arguments)] +async fn run_full_keygen_protocol< + 'a, + E: Curve, + S: SecurityLevel, + H: Digest + Clone + Send + 'static, + KBE: KeystoreBackend, + N: Network, +>( + protocol_message_channel: UnboundedReceiver, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + mapping: Arc>, + my_role_id: ecdsa::Public, + network: N, + mut tracer: PerfProfiler, + eid: dfns_cggmp21::ExecutionId<'a>, + aux_eid: dfns_cggmp21::ExecutionId<'a>, + i: u16, + n: u16, + t: u16, + rng: StdRng, + logger: &DebugLogger, + job_id_bytes: &[u8], + key_store: &ECDSAKeyStore, + role_type: RoleType, +) -> Result< + ( + Vec, + Vec, + UnboundedSender, + futures::channel::mpsc::UnboundedReceiver, + ), + JobError, +> { + let (tx0, rx0, tx1, rx1, tx2, rx2) = + gadget_common::channels::create_job_manager_to_async_protocol_channel_split_io_triplex( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping, + my_role_id, + network, + logger.clone(), + ); + let delivery = (rx0, tx0); + let party = MpcParty::, _, _>::connected(delivery); + let incomplete_key_share = + run_and_serialize_keygen::(&mut tracer, eid, i, n, t, party, rng.clone()) + .await?; + let (mut tracer, pregenerated_primes) = + handle_post_incomplete_keygen::(&tracer, logger, job_id_bytes, key_store).await?; + + logger.info(format!("Will now run Keygen protocol: {role_type:?}")); + + let delivery = (rx1, tx1); + let party = MpcParty::, _, _>::connected(delivery); + let (key_share, serialized_public_key) = run_and_serialize_keyrefresh::( + logger, + incomplete_key_share, + pregenerated_primes, + &mut tracer, + aux_eid, + i, + n, + party, + rng, + ) + .await?; - return Some(key); - } - None + Ok((key_share, serialized_public_key, tx2, rx2)) } diff --git a/protocols/dfns-cggmp21/src/protocols/mod.rs b/protocols/dfns-cggmp21/src/protocols/mod.rs index 76127e8f4..82ff2039f 100644 --- a/protocols/dfns-cggmp21/src/protocols/mod.rs +++ b/protocols/dfns-cggmp21/src/protocols/mod.rs @@ -1,5 +1,11 @@ +use dfns_cggmp21::security_level::SecurityLevel128; +use sha2::Sha256; + pub mod key_refresh; pub mod key_rotate; pub mod keygen; pub mod sign; pub mod util; + +pub type DefaultSecurityLevel = SecurityLevel128; +pub type DefaultCryptoHasher = Sha256; diff --git a/protocols/dfns-cggmp21/src/protocols/sign.rs b/protocols/dfns-cggmp21/src/protocols/sign.rs index 4e94dcef0..224d8b833 100644 --- a/protocols/dfns-cggmp21/src/protocols/sign.rs +++ b/protocols/dfns-cggmp21/src/protocols/sign.rs @@ -1,6 +1,13 @@ -use dfns_cggmp21::supported_curves::Secp256k1; -use dfns_cggmp21::KeyShare; +use dfns_cggmp21::generic_ec::coords::HasAffineX; +use dfns_cggmp21::generic_ec::{Curve, Point}; +use dfns_cggmp21::round_based::{Delivery, MpcParty}; +use dfns_cggmp21::security_level::SecurityLevel; +use dfns_cggmp21::signing::msg::Msg; + +use dfns_cggmp21::supported_curves::{Secp256k1, Secp256r1, Stark}; +use dfns_cggmp21::{DataToSign, KeyShare}; use gadget_common::client::{ClientWithApi, JobsApiForGadget}; +use gadget_common::config::DebugLogger; use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; use gadget_common::gadget::work_manager::WorkManager; use gadget_common::gadget::JobInitMetadata; @@ -9,8 +16,14 @@ use gadget_common::prelude::{FullProtocolConfig, Network}; use gadget_common::Block; use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; -use rand::SeedableRng; +use rand::{CryptoRng, RngCore, SeedableRng}; + use sc_client_api::Backend; + +use crate::protocols::{DefaultCryptoHasher, DefaultSecurityLevel}; +use dfns_cggmp21::signing::SigningBuilder; +use digest::typenum::U32; +use digest::Digest; use sp_api::ProvideRuntimeApi; use sp_core::{ecdsa, keccak_256, Pair}; use std::collections::HashMap; @@ -18,9 +31,11 @@ use std::sync::Arc; use tangle_primitives::jobs::{ DKGTSSSignatureResult, DigitalSignatureScheme, JobId, JobResult, JobType, }; -use tangle_primitives::roles::RoleType; +use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; use tokio::sync::mpsc::UnboundedReceiver; +use super::keygen::create_party; + pub async fn create_next_job< B: Block, BE: Backend + 'static, @@ -74,7 +89,7 @@ where signers, job_id, role_type: job.role_type, - key, + serialized_key_share: key, input_data_to_sign, user_id_to_account_id_mapping, }; @@ -88,11 +103,55 @@ pub struct DfnsCGGMP21SigningExtraParams { signers: Vec, job_id: JobId, role_type: RoleType, - key: KeyShare, + serialized_key_share: Vec, input_data_to_sign: Vec, user_id_to_account_id_mapping: Arc>, } +#[allow(clippy::too_many_arguments)] +pub async fn run_and_serialize_signing<'r, E, L, R, D, H>( + logger: &DebugLogger, + tracer: &mut dfns_cggmp21::progress::PerfProfiler, + eid: dfns_cggmp21::ExecutionId<'r>, + i: u16, + signers: Vec, + msg: DataToSign, + key: Vec, + party: MpcParty, D>, + rng: &mut R, +) -> Result, JobError> +where + E: Curve, + Point: HasAffineX, + L: SecurityLevel, + R: RngCore + CryptoRng, + D: Delivery>, + H: Digest + Clone + Send + 'static, +{ + let key: KeyShare = bincode2::deserialize(&key).map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + })?; + let signing_builder = SigningBuilder::::new(eid, i, &signers, &key); + let signature = signing_builder + .set_progress_tracer(tracer) + .sign(rng, party, msg) + .await + .map_err(|err| JobError { + reason: format!("Signing protocol error: {err:?}"), + })?; + + logger.info("Done signing"); + + let perf_report = tracer.get_report().map_err(|err| JobError { + reason: format!("Signing protocol error: {err:?}"), + })?; + logger.trace(format!("Signing protocol report: {perf_report}")); + // Normalize the signature + let mut ret = [0u8; 65]; + signature.write_to_slice(&mut ret); + Ok(ret.to_vec()) +} + pub async fn generate_protocol_from< B: Block, BE: Backend + 'static, @@ -119,24 +178,24 @@ where let my_role_id = config.key_store.pair().public(); let network = config.clone(); - let (i, signers, t, key, input_data_to_sign, mapping) = ( + let (i, signers, t, serialized_key_share, role_type, input_data_to_sign, mapping) = ( additional_params.i, additional_params.signers, additional_params.t, - additional_params.key, + additional_params.serialized_key_share, + additional_params.role_type, additional_params.input_data_to_sign.clone(), additional_params.user_id_to_account_id_mapping.clone(), ); - let public_key_bytes = key.shared_public_key().to_bytes(true).to_vec(); - let input_data_to_sign2 = input_data_to_sign.clone(); + let public_key = get_public_key_from_serialized_key_share_bytes::( + &role_type, + &serialized_key_share, + )?; Ok(JobBuilder::new() .protocol(async move { let mut rng = rand::rngs::StdRng::from_entropy(); - let protocol_message_channel = - super::util::CloneableUnboundedReceiver::from(protocol_message_channel); - logger.info(format!( "Starting Signing Protocol with params: i={i}, t={t}" )); @@ -145,43 +204,123 @@ where let mix = keccak_256(b"dnfs-cggmp21-signing"); let eid_bytes = [&job_id_bytes[..], &mix[..]].concat(); let eid = dfns_cggmp21::ExecutionId::new(&eid_bytes); - let ( - signing_tx_to_outbound, - signing_rx_async_proto, - _broadcast_tx_to_outbound, - _broadcast_rx_from_gadget, - ) = super::util::create_job_manager_to_async_protocol_channel_split::<_, (), _>( - protocol_message_channel.clone(), - associated_block_id, - associated_retry_id, - associated_session_id, - associated_task_id, - mapping.clone(), - my_role_id, - network.clone(), - ); let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); - let delivery = (signing_rx_async_proto, signing_tx_to_outbound); - let party = dfns_cggmp21::round_based::MpcParty::connected(delivery); let data_hash = keccak_256(&input_data_to_sign); - let data_to_sign = dfns_cggmp21::DataToSign::from_scalar( - dfns_cggmp21::generic_ec::Scalar::from_be_bytes_mod_order(data_hash), - ); - let signature = dfns_cggmp21::signing(eid, i, &signers, &key) - .set_progress_tracer(&mut tracer) - .sign(&mut rng, party, data_to_sign) - .await - .map_err(|err| JobError { - reason: format!("Signing protocol error: {err:?}"), - })?; - - let perf_report = tracer.get_report().map_err(|err| JobError { - reason: format!("Signing protocol error: {err:?}"), - })?; - logger.trace(format!("Signing protocol report: {perf_report}")); - // Normalize the signature - let signature = signature.normalize_s(); + + let signature = match role_type { + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1) => { + let data_to_sign = DataToSign::from_scalar(dfns_cggmp21::generic_ec::Scalar::< + Secp256k1, + >::from_be_bytes_mod_order( + data_hash + )); + let (_, _, party) = create_party::< + Secp256k1, + _, + DefaultSecurityLevel, + Msg, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + my_role_id, + network.clone(), + logger.clone(), + ); + run_and_serialize_signing::<_, DefaultSecurityLevel, _, _, DefaultCryptoHasher>( + &logger, + &mut tracer, + eid, + i, + signers, + data_to_sign, + serialized_key_share, + party, + &mut rng, + ) + .await? + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256r1) => { + let data_to_sign = DataToSign::from_scalar(dfns_cggmp21::generic_ec::Scalar::< + Secp256r1, + >::from_be_bytes_mod_order( + data_hash + )); + let (_, _, party) = create_party::< + Secp256r1, + _, + DefaultSecurityLevel, + Msg, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + my_role_id, + network.clone(), + logger.clone(), + ); + run_and_serialize_signing::<_, DefaultSecurityLevel, _, _, DefaultCryptoHasher>( + &logger, + &mut tracer, + eid, + i, + signers, + data_to_sign, + serialized_key_share, + party, + &mut rng, + ) + .await? + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Stark) => { + let data_to_sign = DataToSign::from_scalar(dfns_cggmp21::generic_ec::Scalar::< + Stark, + >::from_be_bytes_mod_order( + data_hash + )); + let (_, _, party) = create_party::< + Stark, + _, + DefaultSecurityLevel, + Msg, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + my_role_id, + network.clone(), + logger.clone(), + ); + run_and_serialize_signing::<_, DefaultSecurityLevel, _, _, DefaultCryptoHasher>( + &logger, + &mut tracer, + eid, + i, + signers, + data_to_sign, + serialized_key_share, + party, + &mut rng, + ) + .await? + } + _ => { + return Err(JobError { + reason: format!("Invalid role type: {role_type:?}"), + }); + } + }; + logger.debug("Finished AsyncProtocol - Signing"); *protocol_output.lock().await = Some(signature); Ok(()) @@ -189,47 +328,17 @@ where .post(async move { // Submit the protocol output to the blockchain if let Some(signature) = protocol_output_clone.lock().await.take() { - let mut signature_bytes = [0u8; 65]; - signature.write_to_slice(&mut signature_bytes[0..64]); - // To figure out the recovery ID, we need to try all possible values of v - // in our case, v can be 0 or 1 - let mut v = 0u8; - loop { - let mut signature_bytes = signature_bytes; - let data_hash = keccak_256(&input_data_to_sign2); - signature_bytes[64] = v; - let res = sp_io::crypto::secp256k1_ecdsa_recover(&signature_bytes, &data_hash); - match res { - Ok(key) if key[..32] == public_key_bytes[1..] => { - // Found the correct v - break; - } - Ok(_) => { - // Found a key, but not the correct one - // Try the other v value - v = 1; - continue; - } - Err(_) if v == 1 => { - // We tried both v values, but no key was found - // This should never happen, but if it does, we will just - // leave v as 1 and break - break; - } - Err(_) => { - // No key was found, try the other v value - v = 1; - continue; - } - } - } - signature_bytes[64] = v + 27; + let signature = convert_dfns_signature( + signature, + &additional_params.input_data_to_sign, + &public_key, + ); let job_result = JobResult::DKGPhaseTwo(DKGTSSSignatureResult { signature_scheme: DigitalSignatureScheme::Ecdsa, data: additional_params.input_data_to_sign.try_into().unwrap(), - signature: signature_bytes.to_vec().try_into().unwrap(), - verifying_key: public_key_bytes.try_into().unwrap(), + signature: signature.to_vec().try_into().unwrap(), + verifying_key: public_key.try_into().unwrap(), }); client @@ -248,3 +357,81 @@ where }) .build()) } + +pub fn get_public_key_from_serialized_key_share_bytes( + role_type: &RoleType, + serialized_key_share: &[u8], +) -> Result, JobError> { + match role_type { + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256k1) => { + get_local_key_share_from_serialized_local_key_bytes::( + serialized_key_share, + ) + .map(|key_share| key_share.shared_public_key().to_bytes(true).to_vec()) + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Secp256r1) => { + get_local_key_share_from_serialized_local_key_bytes::( + serialized_key_share, + ) + .map(|key_share| key_share.shared_public_key().to_bytes(true).to_vec()) + } + RoleType::Tss(ThresholdSignatureRoleType::DfnsCGGMP21Stark) => { + get_local_key_share_from_serialized_local_key_bytes::(serialized_key_share) + .map(|key_share| key_share.shared_public_key().to_bytes(true).to_vec()) + } + _ => Err(JobError { + reason: format!("Invalid role type: {role_type:?}"), + }), + } +} + +pub fn get_local_key_share_from_serialized_local_key_bytes( + local_key_serialized: &[u8], +) -> Result, JobError> { + bincode2::deserialize::>(local_key_serialized).map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + }) +} + +pub fn convert_dfns_signature( + signature: Vec, + input_data_to_sign: &[u8], + public_key_bytes: &[u8], +) -> [u8; 65] { + let mut signature_bytes = [0u8; 65]; + (signature_bytes[..64]).copy_from_slice(&signature[..64]); + let data_hash = keccak_256(input_data_to_sign); + // To figure out the recovery ID, we need to try all possible values of v + // in our case, v can be 0 or 1 + let mut v = 0u8; + loop { + let mut signature_bytes = signature_bytes; + signature_bytes[64] = v; + let res = sp_io::crypto::secp256k1_ecdsa_recover(&signature_bytes, &data_hash); + match res { + Ok(key) if key[..32] == public_key_bytes[1..] => { + // Found the correct v + break; + } + Ok(_) => { + // Found a key, but not the correct one + // Try the other v value + v = 1; + continue; + } + Err(_) if v == 1 => { + // We tried both v values, but no key was found + // This should never happen, but if it does, we will just + // leave v as 1 and break + break; + } + Err(_) => { + // No key was found, try the other v value + v = 1; + continue; + } + } + } + signature_bytes[64] = v + 27; + signature_bytes +} diff --git a/protocols/dfns-cggmp21/src/protocols/state_machine.rs b/protocols/dfns-cggmp21/src/protocols/state_machine.rs deleted file mode 100644 index db6c6ab9a..000000000 --- a/protocols/dfns-cggmp21/src/protocols/state_machine.rs +++ /dev/null @@ -1,163 +0,0 @@ -use gadget_common::debug_logger::DebugLogger; -use multi_party_ecdsa::gg_2020::state_machine::traits::RoundBlame; -use multi_party_ecdsa::MessageRoundID; -use round_based::{Msg, StateMachine}; -use sp_application_crypto::serde::Serialize; -use std::collections::HashSet; -use std::fmt::Debug; - -pub(crate) struct StateMachineWrapper { - sm: T, - current_round_blame: tokio::sync::watch::Sender, - // stores a list of received messages - received_messages: HashSet>, - logger: DebugLogger, -} - -impl StateMachineWrapper { - pub fn new( - sm: T, - current_round_blame: tokio::sync::watch::Sender, - logger: DebugLogger, - ) -> Self { - Self { - sm, - current_round_blame, - logger, - received_messages: HashSet::new(), - } - } -} - -impl StateMachine for StateMachineWrapper -where - T: StateMachine + RoundBlame + Debug, - ::Err: Debug, - ::MessageBody: Serialize + MessageRoundID, -{ - type Err = T::Err; - type Output = T::Output; - type MessageBody = T::MessageBody; - - fn handle_incoming(&mut self, msg: Msg) -> Result<(), Self::Err> { - let (round, sender, _receiver) = (msg.body.round_id(), msg.sender, msg.receiver); - - self.logger.trace(format!( - "Handling incoming message round={}, sender={}", - round, sender - )); - - if round < self.current_round() { - self.logger - .trace(format!("Message for round={round} is outdated, ignoring",)); - return Ok(()); - } - - // Before passing to the state machine, make sure that we haven't already received the same - // message (this is needed as we use a gossiping protocol to send messages, and we don't - // want to process the same message twice) - let msg_serde = bincode2::serialize(&msg).expect("Failed to serialize message"); - if !self.received_messages.insert(msg_serde) { - self.logger.trace(format!( - "Already received message for round={}, sender={}", - round, sender - )); - return Ok(()); - } - - let result = self.sm.handle_incoming(msg.clone()); - - if let Some(err) = result.as_ref().err() { - self.logger.error(format!("StateMachine error: {err:?}")); - } - - // Get the round blame to update round blame - let round_blame = self.round_blame(); - - self.logger.trace(format!( - "SM After: {:?} || round_blame: {:?}", - &self.sm, round_blame - )); - - result - } - - fn message_queue(&mut self) -> &mut Vec> { - self.sm.message_queue() - } - - fn wants_to_proceed(&self) -> bool { - self.sm.wants_to_proceed() - } - - fn proceed(&mut self) -> Result<(), Self::Err> { - self.logger.trace(format!( - "Trying to proceed: current round ({:?}), waiting for msgs from parties: ({:?})", - self.current_round(), - self.round_blame(), - )); - let now = std::time::Instant::now(); - let result = self.sm.proceed(); - - let elapsed = now.elapsed(); - self.logger.trace(format!( - "Proceeded through SM in {}ms. New current round ({:?}), waiting for msgs from parties: ({:?})", - elapsed.as_millis(), - self.current_round(), - self.round_blame(), - )); - - result - } - - fn round_timeout(&self) -> Option { - self.sm.round_timeout() - } - - fn round_timeout_reached(&mut self) -> Self::Err { - self.sm.round_timeout_reached() - } - - fn is_finished(&self) -> bool { - self.sm.is_finished() - } - - fn pick_output(&mut self) -> Option> { - self.sm.pick_output() - } - - fn current_round(&self) -> u16 { - self.sm.current_round() - } - - fn total_rounds(&self) -> Option { - self.sm.total_rounds() - } - - fn party_ind(&self) -> u16 { - self.sm.party_ind() - } - - fn parties(&self) -> u16 { - self.sm.parties() - } -} - -impl RoundBlame for StateMachineWrapper { - fn round_blame(&self) -> (u16, Vec) { - let (unreceived_messages, blamed_parties) = self.sm.round_blame(); - self.logger - .debug(format!("Not received messages from : {blamed_parties:?}")); - let _ = self.current_round_blame.send(CurrentRoundBlame { - unreceived_messages, - blamed_parties: blamed_parties.clone(), - }); - (unreceived_messages, blamed_parties) - } -} - -#[derive(Default, Debug)] -pub struct CurrentRoundBlame { - pub unreceived_messages: u16, - pub blamed_parties: Vec, -} diff --git a/protocols/dfns-cggmp21/src/protocols/util.rs b/protocols/dfns-cggmp21/src/protocols/util.rs index 946db3e1f..49d4a2932 100644 --- a/protocols/dfns-cggmp21/src/protocols/util.rs +++ b/protocols/dfns-cggmp21/src/protocols/util.rs @@ -1,458 +1,10 @@ -#![allow(clippy::type_complexity, clippy::too_many_arguments)] -//! When delivering messages to an async protocol, we want o make sure we don't mix up voting and public key gossip messages -//! Thus, this file contains a function that takes a channel from the gadget to the async protocol and splits it into two channels -use dfns_cggmp21::round_based::{Incoming, MessageDestination, MessageType, Outgoing, PartyIndex}; -use futures::{Stream, StreamExt}; - -use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; -use gadget_common::gadget::network::Network; -use gadget_common::gadget::work_manager::WorkManager; -use gadget_core::job_manager::WorkManagerInterface; -use rand::seq::SliceRandom; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use gadget_common::gadget::message::UserID; +use rand::prelude::SliceRandom; use sp_core::ecdsa; +use sp_core::ecdsa::Public; use std::collections::HashMap; -use std::sync::atomic::AtomicBool; -use std::sync::Arc; -use tokio::sync::mpsc::UnboundedReceiver; - -/// A Channel Receiver that can be cloned. -/// -/// On the second clone, the original channel will stop sending messages -/// and the new channel will start sending messages. -pub struct CloneableUnboundedReceiver { - rx: Arc>>, - is_in_use: Arc, -} - -impl Clone for CloneableUnboundedReceiver { - fn clone(&self) -> Self { - // on the clone, we switch the is_in_use flag to false - // and we return a new channel - self.is_in_use - .store(false, std::sync::atomic::Ordering::SeqCst); - Self { - rx: self.rx.clone(), - is_in_use: Arc::new(AtomicBool::new(true)), - } - } -} - -impl From> for CloneableUnboundedReceiver { - fn from(rx: UnboundedReceiver) -> Self { - Self { - rx: Arc::new(tokio::sync::Mutex::new(rx)), - is_in_use: Arc::new(AtomicBool::new(false)), - } - } -} - -impl Stream for CloneableUnboundedReceiver { - type Item = T; - fn poll_next( - self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - if !self.is_in_use.load(std::sync::atomic::Ordering::SeqCst) { - return std::task::Poll::Ready(None); - } - let mut rx = match self.rx.try_lock() { - Ok(rx) => rx, - Err(_) => return std::task::Poll::Pending, - }; - let rx = &mut *rx; - tokio::pin!(rx); - rx.poll_recv(cx) - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub enum SplitChannelMessage { - Channel1(C1), - Channel2(C2), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct VotingMessage { - pub from: UserID, - pub to: Option, - pub payload: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct PublicKeyGossipMessage { - pub from: UserID, - pub to: Option, - pub signature: Vec, - pub id: ecdsa::Public, -} - -/// All possible senders of a message -#[derive(Debug, Default, Serialize, Deserialize)] -pub enum MaybeSender { - /// We are the sender of the message - Myself, - /// The sender is someone else - /// it could also be us, double check the [`UserID`] - SomeoneElse(UserID), - /// The sender is unknown. - #[default] - Unknown, -} - -impl MaybeSender { - /// Returns `true` if the maybe sender is [`Myself`]. - /// - /// [`Myself`]: MaybeSender::Myself - #[must_use] - pub fn is_myself(&self) -> bool { - matches!(self, Self::Myself) - } - - /// Returns `true` if the maybe sender is [`Myself`]. - /// Or if the sender is [`SomeoneElse`] but the [`UserID`] is the same as `my_user_id` - /// - /// [`Myself`]: MaybeSender::Myself - /// [`SomeoneElse`]: MaybeSender::SomeoneElse - #[must_use] - pub fn is_myself_check(&self, my_user_id: UserID) -> bool { - match self { - Self::Myself => true, - Self::SomeoneElse(id) if (*id == my_user_id) => true, - _ => false, - } - } - - /// Returns `true` if the maybe sender is [`SomeoneElse`]. - /// - /// [`SomeoneElse`]: MaybeSender::SomeoneElse - #[must_use] - pub fn is_someone_else(&self) -> bool { - matches!(self, Self::SomeoneElse(..)) - } - - /// Returns `true` if the maybe sender is [`Unknown`]. - /// - /// [`Unknown`]: MaybeSender::Unknown - #[must_use] - pub fn is_unknown(&self) -> bool { - matches!(self, Self::Unknown) - } - - /// Returns the sender as [`UserID`] if it is knwon. - #[must_use] - pub fn as_user_id(&self) -> Option { - match self { - Self::Myself => None, - Self::SomeoneElse(id) => Some(*id), - Self::Unknown => None, - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub enum MaybeReceiver { - /// The message is broadcasted to everyone - Broadcast, - /// The message is sent to a specific party - P2P(UserID), - /// The receiver is us. - Myself, - /// The receiver is unknown. - #[default] - Unknown, -} - -impl MaybeReceiver { - /// Returns `true` if the maybe receiver is [`Broadcast`]. - /// - /// [`Broadcast`]: MaybeReceiver::Broadcast - #[must_use] - pub fn is_broadcast(&self) -> bool { - matches!(self, Self::Broadcast) - } - - /// Returns `true` if the maybe receiver is [`P2P`]. - /// - /// [`P2P`]: MaybeReceiver::P2P - #[must_use] - pub fn is_p2_p(&self) -> bool { - matches!(self, Self::P2P(..)) - } - - /// Returns `true` if the maybe receiver is [`Myself`]. - /// - /// [`Myself`]: MaybeReceiver::Myself - #[must_use] - pub fn is_myself(&self) -> bool { - matches!(self, Self::Myself) - } - - /// Returns `true` if the maybe receiver is [`Myself`] - /// Or if the receiver is [`P2P`] but the [`UserID`] is the same as `my_user_id` - /// - /// [`Myself`]: MaybeReceiver::Myself - /// [`P2P`]: MaybeReceiver::P2P - #[must_use] - pub fn is_myself_check(&self, my_user_id: UserID) -> bool { - match self { - Self::Myself => true, - Self::P2P(id) if (*id == my_user_id) => true, - _ => false, - } - } - - /// Returns `true` if the maybe receiver is [`Unknown`]. - /// - /// [`Unknown`]: MaybeReceiver::Unknown - #[must_use] - pub fn is_unknown(&self) -> bool { - matches!(self, Self::Unknown) - } - /// Returns the receiver as [`UserID`] if it is knwon. - #[must_use] - pub fn as_user_id(&self) -> Option { - match self { - Self::Broadcast => None, - Self::P2P(id) => Some(*id), - Self::Myself => None, - Self::Unknown => None, - } - } -} - -/// A Simple trait to extract the sender and the receiver from a message -pub trait MaybeSenderReceiver { - fn maybe_sender(&self) -> MaybeSender; - fn maybe_receiver(&self) -> MaybeReceiver; -} - -impl MaybeSenderReceiver for PublicKeyGossipMessage { - fn maybe_sender(&self) -> MaybeSender { - MaybeSender::SomeoneElse(self.from) - } - fn maybe_receiver(&self) -> MaybeReceiver { - match self.to { - Some(id) => MaybeReceiver::P2P(id), - None => MaybeReceiver::Broadcast, - } - } -} - -impl MaybeSenderReceiver for VotingMessage { - fn maybe_sender(&self) -> MaybeSender { - MaybeSender::SomeoneElse(self.from) - } - fn maybe_receiver(&self) -> MaybeReceiver { - match self.to { - Some(id) => MaybeReceiver::P2P(id), - None => MaybeReceiver::Broadcast, - } - } -} - -impl MaybeSenderReceiver for Outgoing { - fn maybe_sender(&self) -> MaybeSender { - MaybeSender::Myself - } - - fn maybe_receiver(&self) -> MaybeReceiver { - match self.recipient { - MessageDestination::AllParties => MaybeReceiver::Broadcast, - MessageDestination::OneParty(i) => MaybeReceiver::P2P(i as UserID), - } - } -} - -impl MaybeSenderReceiver for Incoming { - fn maybe_sender(&self) -> MaybeSender { - MaybeSender::SomeoneElse(self.sender as UserID) - } - - fn maybe_receiver(&self) -> MaybeReceiver { - match self.msg_type { - MessageType::Broadcast => MaybeReceiver::Broadcast, - MessageType::P2P => MaybeReceiver::Myself, - } - } -} - -impl MaybeSenderReceiver for () { - fn maybe_sender(&self) -> MaybeSender { - MaybeSender::Unknown - } - - fn maybe_receiver(&self) -> MaybeReceiver { - MaybeReceiver::Unknown - } -} - -pub(crate) fn create_job_manager_to_async_protocol_channel_split< - N: Network + 'static, - C2: Serialize + DeserializeOwned + MaybeSenderReceiver + Send + 'static, - M: Serialize + DeserializeOwned + Send + 'static, ->( - mut rx_gadget: CloneableUnboundedReceiver, - associated_block_id: ::Clock, - associated_retry_id: ::RetryID, - associated_session_id: ::SessionID, - associated_task_id: ::TaskID, - user_id_mapping: Arc>, - my_role_id: ecdsa::Public, - network: N, -) -> ( - futures::channel::mpsc::UnboundedSender>, - futures::channel::mpsc::UnboundedReceiver< - Result, futures::channel::mpsc::TryRecvError>, - >, - futures::channel::mpsc::UnboundedSender, - futures::channel::mpsc::UnboundedReceiver, -) { - let (tx_to_async_proto_1, rx_for_async_proto_1) = futures::channel::mpsc::unbounded(); - let (tx_to_async_proto_2, rx_for_async_proto_2) = futures::channel::mpsc::unbounded(); - - // Take the messages from the gadget and send them to the async protocol - tokio::task::spawn(async move { - let mut id = 0; - while let Some(msg_orig) = rx_gadget.next().await { - if msg_orig.payload.is_empty() { - log::warn!(target: "gadget", "Received empty message from Peer {}", msg_orig.from); - continue; - } - match bincode2::deserialize::>(&msg_orig.payload) { - Ok(msg) => match msg { - SplitChannelMessage::Channel1(msg) => { - let msg_type = if msg_orig.to.is_some() { - MessageType::P2P - } else { - MessageType::Broadcast - }; - let incoming = Incoming { - id, - sender: msg_orig.from as PartyIndex, - msg_type, - msg, - }; - - if tx_to_async_proto_1.unbounded_send(Ok(incoming)).is_err() { - log::error!(target: "gadget", "Failed to send Incoming message to protocol"); - } - - id += 1; - } - SplitChannelMessage::Channel2(msg) => { - if tx_to_async_proto_2.unbounded_send(msg).is_err() { - log::error!(target: "gadget", "Failed to send C2 message to protocol"); - } - } - }, - Err(err) => { - log::error!(target: "gadget", "Failed to deserialize message: {err:?}"); - } - } - } - }); - - let (tx_to_outbound_1, mut rx_to_outbound_1) = - futures::channel::mpsc::unbounded::>(); - let (tx_to_outbound_2, mut rx_to_outbound_2) = futures::channel::mpsc::unbounded::(); - let network_clone = network.clone(); - let user_id_mapping_clone = user_id_mapping.clone(); - let my_user_id = user_id_mapping - .iter() - .find_map(|(user_id, account_id)| { - if *account_id == my_role_id { - Some(*user_id) - } else { - None - } - }) - .expect("Failed to find my user id"); - // Take the messages from the async protocol and send them to the gadget - tokio::task::spawn(async move { - let offline_task = async move { - while let Some(msg) = rx_to_outbound_1.next().await { - let from = msg.maybe_sender(); - let to = msg.maybe_receiver(); - let (to_account_id, from_account_id) = get_to_and_from_account_id( - &user_id_mapping_clone, - from.as_user_id().unwrap_or(my_user_id), - to.as_user_id(), - ); - let msg = SplitChannelMessage::::Channel1(msg.msg); - let msg = GadgetProtocolMessage { - associated_block_id, - associated_session_id, - associated_retry_id, - task_hash: associated_task_id, - from: from.as_user_id().unwrap_or(my_user_id), - to: to.as_user_id(), - payload: bincode2::serialize(&msg).expect("Failed to serialize message"), - from_network_id: from_account_id, - to_network_id: to_account_id, - }; - - if let Err(err) = network.send_message(msg).await { - log::error!(target:"gadget", "Failed to send message to outbound: {err:?}"); - } - } - }; - - let voting_task = async move { - while let Some(msg) = rx_to_outbound_2.next().await { - let from = msg.maybe_sender(); - let to = msg.maybe_receiver(); - let (to_account_id, from_account_id) = get_to_and_from_account_id( - &user_id_mapping, - from.as_user_id().unwrap_or(my_user_id), - to.as_user_id(), - ); - let msg = SplitChannelMessage::::Channel2(msg); - let msg = GadgetProtocolMessage { - associated_block_id, - associated_session_id, - associated_retry_id, - task_hash: associated_task_id, - from: from.as_user_id().unwrap_or(my_user_id), - to: to.as_user_id(), - payload: bincode2::serialize(&msg).expect("Failed to serialize message"), - from_network_id: from_account_id, - to_network_id: to_account_id, - }; - - if let Err(err) = network_clone.send_message(msg).await { - log::error!(target:"gadget", "Failed to send message to outbound: {err:?}"); - } - } - }; - - tokio::join!(offline_task, voting_task); - }); - - ( - tx_to_outbound_1, - rx_for_async_proto_1, - tx_to_outbound_2, - rx_for_async_proto_2, - ) -} - -fn get_to_and_from_account_id( - mapping: &HashMap, - from: UserID, - to: Option, -) -> (Option, Option) { - let from_account_id = mapping.get(&from).cloned(); - let to_account_id = if let Some(to) = to { - mapping.get(&to).cloned() - } else { - None - }; - - (to_account_id, from_account_id) -} +pub type ChosenSigners = (u16, Vec, HashMap); /// Given a list of participants, choose `t` of them and return the index of the current participant /// and the indices of the chosen participants, as well as a mapping from the index to the account @@ -469,7 +21,7 @@ pub fn choose_signers( my_role_key: &ecdsa::Public, participants: &[ecdsa::Public], t: u16, -) -> Result<(u16, Vec, HashMap), gadget_common::Error> { +) -> Result { let selected_participants = participants .choose_multiple(rng, t as usize) .cloned() diff --git a/protocols/dfns-cggmp21/tests/dfns.rs b/protocols/dfns-cggmp21/tests/dfns.rs index 2a6c4f9b4..3ede83883 100644 --- a/protocols/dfns-cggmp21/tests/dfns.rs +++ b/protocols/dfns-cggmp21/tests/dfns.rs @@ -4,7 +4,7 @@ mod tests { use futures::StreamExt; use tangle_primitives::jobs::{ DKGTSSPhaseFourJobType, DKGTSSPhaseOneJobType, DKGTSSPhaseThreeJobType, - DKGTSSPhaseTwoJobType, JobId, JobSubmission, JobType, + DKGTSSPhaseTwoJobType, FallbackOptions, JobId, JobSubmission, JobType, }; use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; use tangle_primitives::AccountId; @@ -72,6 +72,7 @@ mod tests { .collect::>(); let submission = JobSubmission { + fallback: FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { @@ -114,6 +115,7 @@ mod tests { .map(AccountId::from) .collect::>(); let submission = JobSubmission { + fallback: FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::DKGTSSPhaseTwo(DKGTSSPhaseTwoJobType { @@ -154,6 +156,7 @@ mod tests { .map(AccountId::from) .collect::>(); let submission = JobSubmission { + fallback: FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::DKGTSSPhaseThree(DKGTSSPhaseThreeJobType { @@ -194,6 +197,7 @@ mod tests { .map(AccountId::from) .collect::>(); let submission = JobSubmission { + fallback: FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::DKGTSSPhaseFour(DKGTSSPhaseFourJobType { diff --git a/protocols/stub/README.md b/protocols/stub/README.md index 9541156d8..d19e2112c 100644 --- a/protocols/stub/README.md +++ b/protocols/stub/README.md @@ -37,4 +37,4 @@ pub struct StubProtocol< At the bottom of lib.rs, add the macro: `generate_setup_and_run_command!(StubProtocol, StubProtocol, ...)`. For real applications, you will want to add all the protocol configs you wish to run concurrently. #### Step 4: Testing the protocol: -After generating the macro above, run this macro with appropriate `T`, `N`, and `K` values: `test_utils::generate_signing_and_keygen_tss_tests!(T, N, K, ThresholdSignatureRoleType::StubProtocol)`. Note: the `K` value is the number of expected protocols to run concurrently. It should match the number of items in the `generate_setup_and_run_command!` macro. \ No newline at end of file +After generating the macro above, run this macro with appropriate `T`, `N`, and `K` values: `test_utils::generate_signing_and_keygen_tss_tests!(T, N, K, ThresholdSignatureRoleType::StubProtocol)`. Note: the `K` value is the number of expected protocols to run concurrently. It should match the number of items in the `generate_setup_and_run_command!` macro. diff --git a/protocols/stub/src/lib.rs b/protocols/stub/src/lib.rs index 6263b3680..9b1f9c583 100644 --- a/protocols/stub/src/lib.rs +++ b/protocols/stub/src/lib.rs @@ -1,7 +1,5 @@ use gadget_common::full_protocol::SharedOptional; -use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend}; use gadget_common::prelude::*; -use gadget_common::ProtocolWorkManager; #[protocol] pub struct StubProtocol< diff --git a/protocols/zcash-frost/Cargo.toml b/protocols/zcash-frost/Cargo.toml new file mode 100644 index 000000000..b2bd6e772 --- /dev/null +++ b/protocols/zcash-frost/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "zcash-frost-protocol" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time", "net"] } +gadget-common = { workspace = true } +gadget-core = { workspace = true } +protocol-macros = { workspace = true } +async-trait = { workspace = true } +log = { workspace = true } +curv = { workspace = true } +futures = { workspace = true } +itertools = { workspace = true } +bincode2 = { workspace = true } +round-based-21 = { workspace = true, features = ["derive"]} +digest = { workspace = true } +sha2 = "0.10" +rand_core = "0.6" +rand_chacha = { version = "0.3", default-features = false } +dfns-cggmp21 = { workspace = true } + +udigest = { workspace = true } +frost-core = { workspace = true, features = ["cheater-detection"] } +frost-ed25519 = { workspace = true } +frost-ed448 = { workspace = true } +frost-p256 = { workspace = true } +frost-p384 = { workspace = true } +frost-ristretto255 = { workspace = true } +frost-secp256k1 = { workspace = true } +frost-rerandomized = { workspace = true } + +pallet-jobs-rpc-runtime-api = { workspace = true, features = ["std"] } +pallet-jobs = { workspace = true, features = ["std"] } +pallet-dkg = { workspace = true, features = ["std"] } +tangle-primitives = { workspace = true, features = ["std"] } + +sp-core = { workspace = true, features = ["std"] } +sp-io = { workspace = true, features = ["std"] } +sp-api = { workspace = true, features = ["std"] } +sp-runtime = { workspace = true, features = ["std"] } +sp-application-crypto = { workspace = true, features = ["std"] } + +sc-client-api = { workspace = true } + +frame-support = { workspace = true } +parity-scale-codec = { workspace = true } + +serde = { version = "1.0.193", features = ["derive"] } +rand = { workspace = true } +hex = { workspace = true } +parking_lot = { workspace = true } +test-utils = { workspace = true } diff --git a/protocols/zcash-frost/src/constants.rs b/protocols/zcash-frost/src/constants.rs new file mode 100644 index 000000000..8db1d8d75 --- /dev/null +++ b/protocols/zcash-frost/src/constants.rs @@ -0,0 +1,29 @@ +// ================= Common ======================== // +pub const ZCASH_FROST_KEYGEN_PROTOCOL_NAME: &str = "/tangle/zcash-frost/keygen/1"; +pub const ZCASH_FROST_SIGNING_PROTOCOL_NAME: &str = "/tangle/zcash-frost/signing/1"; + +// ============= Signing Protocol ======================= // + +pub mod signing_worker { + use std::time::Duration; + + // the maximum number of tasks that the work manager tries to assign + pub const MAX_RUNNING_TASKS: usize = 2; + + // the maximum number of tasks that can be enqueued, + // enqueued here implies not actively running but listening for messages + pub const MAX_ENQUEUED_TASKS: usize = 10; + + // How often to poll the jobs to check completion status + pub const JOB_POLL_INTERVAL: Duration = Duration::from_millis(500); +} + +// ============= Keygen Protocol ======================= // + +pub mod keygen_worker { + /// the maximum number of tasks that the work manager tries to assign + /// at any given time for the keygen protocol. + pub const MAX_RUNNING_TASKS: usize = 2; + /// the maximum number of tasks that can be enqueued. + pub const MAX_ENQUEUED_TASKS: usize = 10; +} diff --git a/protocols/zcash-frost/src/lib.rs b/protocols/zcash-frost/src/lib.rs new file mode 100644 index 000000000..b57d61a04 --- /dev/null +++ b/protocols/zcash-frost/src/lib.rs @@ -0,0 +1,96 @@ +use crate::protocol::keygen::ZcashFrostKeygenExtraParams; +use crate::protocol::sign::ZcashFrostSigningExtraParams; +use async_trait::async_trait; +use gadget_common::full_protocol::SharedOptional; +use gadget_common::prelude::*; +use gadget_common::{generate_protocol, generate_setup_and_run_command}; +use protocol_macros::protocol; + +pub mod constants; +pub mod protocol; +pub mod rounds; + +generate_protocol!( + "Zcash-FROST-Keygen-Protocol", + ZcashFrostKeygenProtocol, + ZcashFrostKeygenExtraParams, + crate::protocol::keygen::generate_protocol_from, + crate::protocol::keygen::create_next_job, + GadgetJobType::DKGTSSPhaseOne(_), + RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostEd25519) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostEd448) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostP256) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostP384) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostSecp256k1) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostRistretto255) +); +generate_protocol!( + "Zcash-FROST-Signing-Protocol", + ZcashFrostSigningProtocol, + ZcashFrostSigningExtraParams, + crate::protocol::sign::generate_protocol_from, + crate::protocol::sign::create_next_job, + GadgetJobType::DKGTSSPhaseTwo(_), + RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostEd25519) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostEd448) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostP256) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostP384) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostSecp256k1) + | RoleType::Tss(ThresholdSignatureRoleType::ZcashFrostRistretto255) +); + +generate_setup_and_run_command!(ZcashFrostKeygenProtocol, ZcashFrostSigningProtocol); + +mod secp256k1 { + test_utils::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 2, + ThresholdSignatureRoleType::ZcashFrostSecp256k1 + ); +} + +mod ristretto255 { + test_utils::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 2, + ThresholdSignatureRoleType::ZcashFrostRistretto255 + ); +} + +mod p256 { + test_utils::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 2, + ThresholdSignatureRoleType::ZcashFrostP256 + ); +} + +mod p384 { + test_utils::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 2, + ThresholdSignatureRoleType::ZcashFrostP384 + ); +} + +mod ed25519 { + test_utils::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 2, + ThresholdSignatureRoleType::ZcashFrostEd25519 + ); +} + +mod ed448 { + test_utils::generate_signing_and_keygen_tss_tests!( + 2, + 3, + 2, + ThresholdSignatureRoleType::ZcashFrostEd448 + ); +} diff --git a/protocols/zcash-frost/src/protocol/keygen.rs b/protocols/zcash-frost/src/protocol/keygen.rs new file mode 100644 index 000000000..48c4985ba --- /dev/null +++ b/protocols/zcash-frost/src/protocol/keygen.rs @@ -0,0 +1,470 @@ +use frost_ed25519::Ed25519Sha512; +use frost_ed448::Ed448Shake256; +use frost_p256::P256Sha256; +use frost_p384::P384Sha384; +use frost_ristretto255::Ristretto255Sha512; +use frost_secp256k1::Secp256K1Sha256; +use futures::StreamExt; +use gadget_common::client::JobsApiForGadget; +use gadget_common::client::{ + ClientWithApi, GadgetJobResult, MaxKeyLen, MaxParticipants, MaxSignatureLen, +}; +use gadget_common::config::{Network, ProvideRuntimeApi}; +use gadget_common::debug_logger::DebugLogger; +use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; +use gadget_common::gadget::work_manager::WorkManager; +use gadget_common::gadget::JobInitMetadata; +use gadget_common::keystore::{ECDSAKeyStore, KeystoreBackend}; +use gadget_common::{channels, Block}; +use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; +use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; +use itertools::Itertools; +use pallet_dkg::signatures_schemes::ecdsa::verify_signer_from_set_ecdsa; +use pallet_dkg::signatures_schemes::to_slice_33; +use rand::SeedableRng; +use round_based_21::{Incoming, Outgoing}; +use sc_client_api::Backend; +use sp_application_crypto::sp_core::keccak_256; +use sp_core::{ecdsa, Pair}; +use std::collections::{BTreeMap, HashMap}; +use std::sync::Arc; +use tangle_primitives::jobs::{DKGTSSKeySubmissionResult, DigitalSignatureScheme, JobId, JobType}; +use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; +use tokio::sync::mpsc::UnboundedReceiver; + +use crate::rounds; +use crate::rounds::keygen::Msg; +use gadget_common::channels::PublicKeyGossipMessage; + +#[derive(Clone)] +pub struct ZcashFrostKeygenExtraParams { + pub i: u16, + pub t: u16, + pub n: u16, + pub job_id: JobId, + pub role_type: RoleType, + pub user_id_to_account_id_mapping: Arc>, +} + +pub async fn create_next_job< + B: Block, + BE: Backend, + C: ClientWithApi, + N: Network, + KBE: KeystoreBackend, +>( + config: &crate::ZcashFrostKeygenProtocol, + job: JobInitMetadata, + _work_manager: &ProtocolWorkManager, +) -> Result +where + >::Api: JobsApiForGadget, +{ + let job_id = job.job_id; + let role_type = job.job_type.get_role_type(); + + // We can safely make this assumption because we are only creating jobs for phase one + let JobType::DKGTSSPhaseOne(p1_job) = job.job_type else { + panic!("Should be valid type") + }; + + let participants = job.participants_role_ids; + let threshold = p1_job.threshold; + + let user_id_to_account_id_mapping = Arc::new( + participants + .clone() + .into_iter() + .enumerate() + .map(|r| (r.0 as UserID, r.1)) + .collect(), + ); + + let id = config.key_store.pair().public(); + + let params = ZcashFrostKeygenExtraParams { + i: participants + .iter() + .position(|p| p == &id) + .expect("Should exist") as u16, + t: threshold as u16, + n: participants.len() as u16, + role_type, + job_id, + user_id_to_account_id_mapping, + }; + + Ok(params) +} + +macro_rules! run_threshold_keygen { + ($impl_type:ty, $tracer:expr, $i:expr, $t:expr, $n:expr, $role:expr, $rng:expr, $party:expr) => { + rounds::keygen::run_threshold_keygen::<$impl_type, _, _>( + Some($tracer), + $i, + $t, + $n, + $role, + $rng, + $party, + ) + .await + .map_err(|err| { + println!("Keygen protocol error: {err:#?}"); + err.to_string() + })? + }; +} + +pub async fn generate_protocol_from< + B: Block, + BE: Backend, + C: ClientWithApi, + N: Network, + KBE: KeystoreBackend, +>( + config: &crate::ZcashFrostKeygenProtocol, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + protocol_message_channel: UnboundedReceiver, + additional_params: ZcashFrostKeygenExtraParams, +) -> Result +where + >::Api: JobsApiForGadget, +{ + let key_store = config.key_store.clone(); + let key_store2 = config.key_store.clone(); + let protocol_output = Arc::new(tokio::sync::Mutex::new(None)); + let protocol_output_clone = protocol_output.clone(); + let pallet_tx = config.pallet_tx.clone(); + let id = config.key_store.pair().public(); + let logger = config.logger.clone(); + let network = config.clone(); + + let (i, t, n, mapping, role_type) = ( + additional_params.i, + additional_params.t, + additional_params.n, + additional_params.user_id_to_account_id_mapping, + additional_params.role_type, + ); + + let role = match role_type { + RoleType::Tss(role) => role, + _ => { + return Err(JobError { + reason: "Invalid role type".to_string(), + }) + } + }; + + Ok(JobBuilder::new() + .protocol(async move { + let mut rng = rand::rngs::StdRng::from_entropy(); + logger.info(format!( + "Starting Keygen Protocol with params: i={i}, t={t}, n={n}" + )); + + let ( + keygen_tx_to_outbound, + keygen_rx_async_proto, + broadcast_tx_to_outbound, + broadcast_rx_from_gadget, + ) = channels::create_job_manager_to_async_protocol_channel_split_io::< + _, + _, + Outgoing, + Incoming, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + id, + network.clone(), + logger.clone(), + ); + let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); + let delivery = (keygen_rx_async_proto, keygen_tx_to_outbound); + let party = round_based_21::MpcParty::connected(delivery); + let frost_key_share_package = match role { + ThresholdSignatureRoleType::ZcashFrostEd25519 => { + run_threshold_keygen!( + Ed25519Sha512, + &mut tracer, + i, + t, + n, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostEd448 => { + run_threshold_keygen!( + Ed448Shake256, + &mut tracer, + i, + t, + n, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostP256 => { + run_threshold_keygen!(P256Sha256, &mut tracer, i, t, n, role, &mut rng, party) + } + ThresholdSignatureRoleType::ZcashFrostP384 => { + run_threshold_keygen!(P384Sha384, &mut tracer, i, t, n, role, &mut rng, party) + } + ThresholdSignatureRoleType::ZcashFrostRistretto255 => { + run_threshold_keygen!( + Ristretto255Sha512, + &mut tracer, + i, + t, + n, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostSecp256k1 => { + run_threshold_keygen!( + Secp256K1Sha256, + &mut tracer, + i, + t, + n, + role, + &mut rng, + party + ) + } + _ => unreachable!("Invalid role"), + }; + let perf_report = tracer.get_report().map_err(|err| JobError { + reason: format!("Keygen protocol error: {err:?}"), + })?; + logger.trace(format!("Incomplete Keygen protocol report: {perf_report}")); + logger.debug("Finished AsyncProtocol - Incomplete Keygen"); + + let job_result = handle_public_key_gossip( + key_store2, + &logger, + &frost_key_share_package.verifying_key, + role, + t, + i, + broadcast_tx_to_outbound, + broadcast_rx_from_gadget, + ) + .await?; + + *protocol_output.lock().await = Some((frost_key_share_package, job_result)); + Ok(()) + }) + .post(async move { + // TODO: handle protocol blames + // Store the keys locally, as well as submitting them to the blockchain + if let Some((local_key, job_result)) = protocol_output_clone.lock().await.take() { + key_store + .set_job_result(additional_params.job_id, local_key) + .await + .map_err(|err| JobError { + reason: format!("Failed to store key: {err:?}"), + })?; + + pallet_tx + .submit_job_result( + additional_params.role_type, + additional_params.job_id, + job_result, + ) + .await + .map_err(|err| JobError { + reason: format!("Failed to submit job result: {err:?}"), + })?; + } + + Ok(()) + }) + .build()) +} + +#[allow(clippy::too_many_arguments)] +async fn handle_public_key_gossip( + key_store: ECDSAKeyStore, + logger: &DebugLogger, + public_key_package: &[u8], + role: ThresholdSignatureRoleType, + t: u16, + i: u16, + broadcast_tx_to_outbound: futures::channel::mpsc::UnboundedSender, + mut broadcast_rx_from_gadget: futures::channel::mpsc::UnboundedReceiver, +) -> Result { + let key_hashed = keccak_256(public_key_package); + let signature = key_store.pair().sign_prehashed(&key_hashed).0.to_vec(); + let my_id = key_store.pair().public(); + let mut received_keys = BTreeMap::new(); + received_keys.insert(i, signature.clone()); + let mut received_participants = BTreeMap::new(); + received_participants.insert(i, my_id); + + broadcast_tx_to_outbound + .unbounded_send(PublicKeyGossipMessage { + from: i as _, + to: None, + signature, + id: my_id, + }) + .map_err(|err| JobError { + reason: format!("Failed to send public key: {err:?}"), + })?; + + for _ in 0..t { + let message = broadcast_rx_from_gadget + .next() + .await + .ok_or_else(|| JobError { + reason: "Failed to receive public key".to_string(), + })?; + + let from = message.from; + logger.debug(format!("Received public key from {from}")); + + if received_keys.contains_key(&(from as u16)) { + logger.warn("Received duplicate key"); + continue; + } + // verify signature + let maybe_signature = sp_core::ecdsa::Signature::from_slice(&message.signature); + match maybe_signature.and_then(|s| s.recover_prehashed(&key_hashed)) { + Some(p) if p != message.id => { + logger.warn(format!( + "Received invalid signature from {from} not signed by them" + )); + } + Some(p) if p == message.id => { + logger.debug(format!("Received valid signature from {from}")); + } + Some(_) => unreachable!("Should not happen"), + None => { + logger.warn(format!("Received invalid signature from {from}")); + continue; + } + } + + received_keys.insert(from as u16, message.signature); + received_participants.insert(from as u16, message.id); + logger.debug(format!( + "Received {}/{} signatures", + received_keys.len(), + t + 1 + )); + } + + // Order and collect the map to ensure symmetric submission to blockchain + let signatures = received_keys + .into_iter() + .sorted_by_key(|x| x.0) + .map(|r| r.1.try_into().unwrap()) + .collect::>(); + + let participants = received_participants + .into_iter() + .sorted_by_key(|x| x.0) + .map(|r| r.1 .0.to_vec().try_into().unwrap()) + .collect::>() + .try_into() + .unwrap(); + + if signatures.len() < t as usize { + return Err(JobError { + reason: format!( + "Received {} signatures, expected at least {}", + signatures.len(), + t + 1, + ), + }); + } + + let res = DKGTSSKeySubmissionResult { + signature_scheme: match role { + ThresholdSignatureRoleType::ZcashFrostEd25519 => DigitalSignatureScheme::SchnorrEd25519, + ThresholdSignatureRoleType::ZcashFrostEd448 => DigitalSignatureScheme::SchnorrEd448, + ThresholdSignatureRoleType::ZcashFrostP256 => DigitalSignatureScheme::SchnorrP256, + ThresholdSignatureRoleType::ZcashFrostP384 => DigitalSignatureScheme::SchnorrP384, + ThresholdSignatureRoleType::ZcashFrostRistretto255 => { + DigitalSignatureScheme::SchnorrRistretto255 + } + ThresholdSignatureRoleType::ZcashFrostSecp256k1 => { + DigitalSignatureScheme::SchnorrSecp256k1 + } + _ => unreachable!("Invalid role"), + }, + key: public_key_package.to_vec().try_into().unwrap(), + participants, + signatures: signatures.try_into().unwrap(), + threshold: t as _, + }; + verify_generated_dkg_key_ecdsa(res.clone(), logger); + Ok(GadgetJobResult::DKGPhaseOne(res)) +} + +fn verify_generated_dkg_key_ecdsa( + data: DKGTSSKeySubmissionResult, + logger: &DebugLogger, +) { + // Ensure participants and signatures are not empty + assert!(!data.participants.is_empty(), "NoParticipantsFound",); + assert!(!data.signatures.is_empty(), "NoSignaturesFound"); + + // Generate the required ECDSA signers + let maybe_signers = data + .participants + .iter() + .map(|x| { + ecdsa::Public( + to_slice_33(x) + .unwrap_or_else(|| panic!("Failed to convert input to ecdsa public key")), + ) + }) + .collect::>(); + + assert!(!maybe_signers.is_empty(), "NoParticipantsFound"); + + let mut known_signers: Vec = Default::default(); + + for signature in data.signatures { + // Ensure the required signer signature exists + let (maybe_authority, success) = + verify_signer_from_set_ecdsa(maybe_signers.clone(), &data.key, &signature); + + if success { + let authority = maybe_authority.expect("CannotRetreiveSigner"); + + // Ensure no duplicate signatures + assert!(!known_signers.contains(&authority), "DuplicateSignature"); + + logger.debug(format!("Verified signature from {}", authority)); + known_signers.push(authority); + } + } + + // Ensure a sufficient number of unique signers are present + assert!( + known_signers.len() > data.threshold as usize, + "NotEnoughSigners" + ); + logger.debug(format!( + "Verified {}/{} signatures", + known_signers.len(), + data.threshold + 1 + )); +} diff --git a/protocols/zcash-frost/src/protocol/mod.rs b/protocols/zcash-frost/src/protocol/mod.rs new file mode 100644 index 000000000..16b9199e9 --- /dev/null +++ b/protocols/zcash-frost/src/protocol/mod.rs @@ -0,0 +1,3 @@ +pub mod keygen; +pub mod sign; +pub mod util; diff --git a/protocols/zcash-frost/src/protocol/sign.rs b/protocols/zcash-frost/src/protocol/sign.rs new file mode 100644 index 000000000..8ffea0b8d --- /dev/null +++ b/protocols/zcash-frost/src/protocol/sign.rs @@ -0,0 +1,390 @@ +use frame_support::BoundedVec; +use frost_core::keys::{KeyPackage, PublicKeyPackage}; +use frost_ed25519::Ed25519Sha512; +use frost_ed448::Ed448Shake256; +use frost_p256::P256Sha256; +use frost_p384::P384Sha384; +use frost_ristretto255::Ristretto255Sha512; +use frost_secp256k1::Secp256K1Sha256; +use gadget_common::client::JobsApiForGadget; +use gadget_common::client::{ClientWithApi, GadgetJobResult}; +use gadget_common::config::{Network, ProvideRuntimeApi}; + +use gadget_common::gadget::message::{GadgetProtocolMessage, UserID}; +use gadget_common::gadget::work_manager::WorkManager; +use gadget_common::gadget::JobInitMetadata; +use gadget_common::keystore::KeystoreBackend; + +use gadget_common::{channels, Block}; +use gadget_core::job::{BuiltExecutableJobWrapper, JobBuilder, JobError}; +use gadget_core::job_manager::{ProtocolWorkManager, WorkManagerInterface}; +use rand::SeedableRng; +use round_based_21::{Incoming, MpcParty, Outgoing}; +use sc_client_api::Backend; +use sp_core::{ecdsa, keccak_256, Pair}; +use std::collections::HashMap; +use std::sync::Arc; +use tangle_primitives::jobs::{DKGTSSSignatureResult, DigitalSignatureScheme, JobId, JobType}; +use tangle_primitives::roles::{RoleType, ThresholdSignatureRoleType}; +use tokio::sync::mpsc::UnboundedReceiver; + +use crate::rounds; +use crate::rounds::keygen::FrostKeyShare; +use crate::rounds::sign::Msg; + +#[derive(Clone)] +pub struct ZcashFrostSigningExtraParams { + pub i: u16, + pub t: u16, + pub signers: Vec, + pub job_id: JobId, + pub role_type: RoleType, + pub keyshare: FrostKeyShare, + pub input_data_to_sign: Vec, + pub user_id_to_account_id_mapping: Arc>, +} + +pub async fn create_next_job< + B: Block, + BE: Backend, + C: ClientWithApi, + N: Network, + KBE: KeystoreBackend, +>( + config: &crate::ZcashFrostSigningProtocol, + job: JobInitMetadata, + _work_manager: &ProtocolWorkManager, +) -> Result +where + >::Api: JobsApiForGadget, +{ + let job_id = job.job_id; + + let JobType::DKGTSSPhaseTwo(p2_job) = job.job_type else { + panic!("Should be valid type") + }; + let input_data_to_sign = p2_job.submission.into(); + let previous_job_id = p2_job.phase_one_id; + + let phase1_job = job.phase1_job.expect("Should exist for a phase 2 job"); + let participants = job.participants_role_ids; + let t = phase1_job.get_threshold().expect("Should exist") as u16; + + let seed = keccak_256(&[&job_id.to_be_bytes()[..], &job.retry_id.to_be_bytes()[..]].concat()); + let mut rng = rand_chacha::ChaChaRng::from_seed(seed); + let id = config.key_store.pair().public(); + + let (i, signers, mapping) = super::util::choose_signers(&mut rng, &id, &participants, t)?; + let key = config + .key_store + .get_job_result(previous_job_id) + .await + .map_err(|err| gadget_common::Error::ClientError { + err: err.to_string(), + })? + .ok_or_else(|| gadget_common::Error::ClientError { + err: format!("No key found for job ID: {job_id:?}"), + })?; + + let user_id_to_account_id_mapping = Arc::new(mapping); + + let params = ZcashFrostSigningExtraParams { + i, + t, + signers, + job_id, + role_type: job.role_type, + keyshare: key, + input_data_to_sign, + user_id_to_account_id_mapping, + }; + Ok(params) +} + +macro_rules! deserialize_and_run_threshold_sign { + ($impl_type:ty, $keyshare:expr, $tracer:expr, $i:expr, $signers:expr, $msg:expr, $role:expr, $rng:expr, $party:expr) => {{ + let key_package = + KeyPackage::<$impl_type>::deserialize(&$keyshare.key_package).map_err(|err| { + JobError { + reason: format!("Failed to deserialize key share: {err:?}"), + } + })?; + + let public_key_package = PublicKeyPackage::<$impl_type>::deserialize( + &$keyshare.pubkey_package, + ) + .map_err(|err| JobError { + reason: format!("Failed to deserialize public key package: {err:?}"), + })?; + + rounds::sign::run_threshold_sign( + Some($tracer), + $i, + $signers, + (key_package, public_key_package), + $msg, + $role, + $rng, + $party, + ) + .await + .map_err(|err| JobError { + reason: format!("Failed to run threshold sign: {err:?}"), + })? + }}; +} + +pub async fn generate_protocol_from< + B: Block, + BE: Backend, + C: ClientWithApi, + N: Network, + KBE: KeystoreBackend, +>( + config: &crate::ZcashFrostSigningProtocol, + associated_block_id: ::Clock, + associated_retry_id: ::RetryID, + associated_session_id: ::SessionID, + associated_task_id: ::TaskID, + protocol_message_channel: UnboundedReceiver, + additional_params: ZcashFrostSigningExtraParams, +) -> Result +where + >::Api: JobsApiForGadget, +{ + let debug_logger_post = config.logger.clone(); + let logger = debug_logger_post.clone(); + let protocol_output = Arc::new(tokio::sync::Mutex::new(None)); + let protocol_output_clone = protocol_output.clone(); + let pallet_tx = config.pallet_tx.clone(); + let id = config.key_store.pair().public(); + let network = config.clone(); + + let (i, signers, t, keyshare, role_type, input_data_to_sign, mapping) = ( + additional_params.i, + additional_params.signers, + additional_params.t, + additional_params.keyshare, + additional_params.role_type, + additional_params.input_data_to_sign.clone(), + additional_params.user_id_to_account_id_mapping.clone(), + ); + + let role = match role_type { + RoleType::Tss(role) => role, + _ => { + return Err(JobError { + reason: "Invalid role type".to_string(), + }) + } + }; + + Ok(JobBuilder::new() + .protocol(async move { + let mut rng = rand::rngs::StdRng::from_entropy(); + logger.info(format!( + "Starting Signing Protocol with params: i={i}, t={t}" + )); + + let ( + signing_tx_to_outbound, + signing_rx_async_proto, + _broadcast_tx_to_outbound, + _broadcast_rx_from_gadget, + ) = channels::create_job_manager_to_async_protocol_channel_split_io::< + _, + (), + Outgoing, + Incoming, + >( + protocol_message_channel, + associated_block_id, + associated_retry_id, + associated_session_id, + associated_task_id, + mapping.clone(), + id, + network.clone(), + logger.clone(), + ); + + let mut tracer = dfns_cggmp21::progress::PerfProfiler::new(); + let delivery = (signing_rx_async_proto, signing_tx_to_outbound); + let party = MpcParty::connected(delivery); + let signature = match role { + ThresholdSignatureRoleType::ZcashFrostSecp256k1 => { + deserialize_and_run_threshold_sign!( + Secp256K1Sha256, + keyshare, + &mut tracer, + i, + signers, + &input_data_to_sign, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostEd25519 => { + deserialize_and_run_threshold_sign!( + Ed25519Sha512, + keyshare, + &mut tracer, + i, + signers, + &input_data_to_sign, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostEd448 => { + deserialize_and_run_threshold_sign!( + Ed448Shake256, + keyshare, + &mut tracer, + i, + signers, + &input_data_to_sign, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostP256 => { + deserialize_and_run_threshold_sign!( + P256Sha256, + keyshare, + &mut tracer, + i, + signers, + &input_data_to_sign, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostP384 => { + deserialize_and_run_threshold_sign!( + P384Sha384, + keyshare, + &mut tracer, + i, + signers, + &input_data_to_sign, + role, + &mut rng, + party + ) + } + ThresholdSignatureRoleType::ZcashFrostRistretto255 => { + deserialize_and_run_threshold_sign!( + Ristretto255Sha512, + keyshare, + &mut tracer, + i, + signers, + &input_data_to_sign, + role, + &mut rng, + party + ) + } + _ => { + return Err(JobError { + reason: "Invalid role type".to_string(), + }) + } + }; + let perf_report = tracer.get_report().map_err(|err| JobError { + reason: format!("Signing protocol error: {err:?}"), + })?; + logger.trace(format!("Signing protocol report: {perf_report}")); + logger.debug("Finished AsyncProtocol - Signing"); + *protocol_output.lock().await = Some(signature); + Ok(()) + }) + .post(async move { + // Submit the protocol output to the blockchain + if let Some(signature) = protocol_output_clone.lock().await.take() { + // Compute the signature bytes by first converting the signature + // to a fixed byte array and then converting that to a Vec. + let (signature, signature_scheme) = match role { + ThresholdSignatureRoleType::ZcashFrostSecp256k1 => { + let mut signature_bytes = [0u8; 65]; + signature_bytes.copy_from_slice(&signature.group_signature); + ( + signature_bytes.to_vec().try_into().unwrap(), + DigitalSignatureScheme::SchnorrSecp256k1, + ) + } + ThresholdSignatureRoleType::ZcashFrostEd25519 => { + let mut signature_bytes = [0u8; 64]; + signature_bytes.copy_from_slice(&signature.group_signature); + ( + signature_bytes.to_vec().try_into().unwrap(), + DigitalSignatureScheme::SchnorrEd25519, + ) + } + ThresholdSignatureRoleType::ZcashFrostEd448 => { + let mut signature_bytes = [0u8; 114]; + signature_bytes.copy_from_slice(&signature.group_signature); + ( + signature_bytes.to_vec().try_into().unwrap(), + DigitalSignatureScheme::SchnorrEd448, + ) + } + ThresholdSignatureRoleType::ZcashFrostP256 => { + let mut signature_bytes = [0u8; 65]; + signature_bytes.copy_from_slice(&signature.group_signature); + ( + signature_bytes.to_vec().try_into().unwrap(), + DigitalSignatureScheme::SchnorrP256, + ) + } + ThresholdSignatureRoleType::ZcashFrostP384 => { + let mut signature_bytes = [0u8; 97]; + signature_bytes.copy_from_slice(&signature.group_signature); + ( + signature_bytes.to_vec().try_into().unwrap(), + DigitalSignatureScheme::SchnorrP384, + ) + } + ThresholdSignatureRoleType::ZcashFrostRistretto255 => { + let mut signature_bytes = [0u8; 64]; + signature_bytes.copy_from_slice(&signature.group_signature); + ( + signature_bytes.to_vec().try_into().unwrap(), + DigitalSignatureScheme::SchnorrRistretto255, + ) + } + _ => { + return Err(JobError { + reason: "Invalid role type".to_string(), + }) + } + }; + + let job_result = GadgetJobResult::DKGPhaseTwo(DKGTSSSignatureResult { + signature_scheme, + data: additional_params.input_data_to_sign.try_into().unwrap(), + signature, + verifying_key: BoundedVec::new(), + }); + + pallet_tx + .submit_job_result( + additional_params.role_type, + additional_params.job_id, + job_result, + ) + .await + .map_err(|err| JobError { + reason: format!("Failed to submit job result: {err:?}"), + })?; + } + + Ok(()) + }) + .build()) +} diff --git a/protocols/zcash-frost/src/protocol/util.rs b/protocols/zcash-frost/src/protocol/util.rs new file mode 100644 index 000000000..5318f1019 --- /dev/null +++ b/protocols/zcash-frost/src/protocol/util.rs @@ -0,0 +1,73 @@ +#![allow(clippy::type_complexity, clippy::too_many_arguments)] +//! When delivering messages to an async protocol, we want to make sure we don't mix up voting and public key gossip messages +//! Thus, this file contains a function that takes a channel from the gadget to the async protocol and splits it into two channels +use gadget_common::gadget::message::UserID; +use rand::seq::SliceRandom; +use sp_core::ecdsa::Public; +use std::collections::HashMap; + +/// Given a list of participants, choose `t` of them and return the index of the current participant +/// and the indices of the chosen participants, as well as a mapping from the index to the account +/// id. +/// +/// # Errors +/// If we are not selected to sign the message it will return an error +/// [`gadget_common::Error::ParticipantNotSelected`]. +/// +/// # Panics +/// If the current participant is not in the list of participants it will panic. +pub fn choose_signers( + rng: &mut R, + my_account_id: &Public, + participants: &[Public], + t: u16, +) -> Result<(u16, Vec, HashMap), gadget_common::Error> { + let selected_participants = participants + .choose_multiple(rng, t as usize) + .cloned() + .collect::>(); + + let selected_participants_indices = selected_participants + .iter() + .map(|p| participants.iter().position(|x| x == p).unwrap() as u16) + .collect::>(); + + let mut selected_participants_with_indices: Vec<(u16, Public)> = selected_participants_indices + .iter() + .cloned() + .zip(selected_participants) + .collect(); + + selected_participants_with_indices.sort_by_key(|&(index, _)| index); + + let (sorted_selected_participants_indices, sorted_selected_participants): ( + Vec, + Vec, + ) = selected_participants_with_indices.into_iter().unzip(); + + let j = participants + .iter() + .position(|p| p == my_account_id) + .expect("Should exist") as u16; + + let i = sorted_selected_participants_indices + .iter() + .position(|p| p == &j) + .map(|i| i as u16) + .ok_or_else(|| gadget_common::Error::ParticipantNotSelected { + id: *my_account_id, + reason: String::from("we are not selected to sign"), + })?; + + let user_id_to_account_id_mapping = sorted_selected_participants + .clone() + .into_iter() + .enumerate() + .map(|(i, p)| (i as UserID, p)) + .collect(); + Ok(( + i, + sorted_selected_participants_indices, + user_id_to_account_id_mapping, + )) +} diff --git a/protocols/zcash-frost/src/rounds/keygen.rs b/protocols/zcash-frost/src/rounds/keygen.rs new file mode 100644 index 000000000..16e05fd02 --- /dev/null +++ b/protocols/zcash-frost/src/rounds/keygen.rs @@ -0,0 +1,268 @@ +use std::collections::BTreeMap; + +use dfns_cggmp21::progress::Tracer; +use frost_core::{ + keys::{ + dkg::{round1, round2}, + KeyPackage, PublicKeyPackage, + }, + Ciphersuite, Identifier, +}; +use futures::SinkExt; +use rand_core::{CryptoRng, RngCore}; +use round_based::{ + rounds_router::{ + simple_store::{RoundInput, RoundMsgs}, + RoundsRouter, + }, + runtime::AsyncRuntime, + Delivery, Mpc, MpcParty, Outgoing, ProtocolMessage, +}; +use round_based_21 as round_based; +use serde::{Deserialize, Serialize}; +use tangle_primitives::roles::ThresholdSignatureRoleType; + +use super::{Error, IoError}; + +/// Message of key generation protocol +#[derive(ProtocolMessage, Clone, Serialize, Deserialize)] +#[serde(bound = "")] +pub enum Msg { + /// Round 1 message + Round1(MsgRound1), + /// Round 2 message + Round2(MsgRound2), + /// Round 3 message + Round3(MsgRound3), +} + +/// Message from round 1 +#[derive(Clone, Serialize, Deserialize, udigest::Digestable)] +#[serde(bound = "")] +#[udigest(bound = "")] +#[udigest(tag = "zcash.frost.keygen.threshold.round1")] +pub struct MsgRound1 { + pub msg: Vec, +} +/// Message from round 2 +#[derive(Clone, Serialize, Deserialize, udigest::Digestable)] +#[serde(bound = "")] +#[udigest(bound = "")] +#[udigest(tag = "zcash.frost.keygen.threshold.round2")] +pub struct MsgRound2 { + pub msg: Vec, +} +/// Message from round 3 +#[derive(Clone, Serialize, Deserialize)] +#[serde(bound = "")] +pub struct MsgRound3 { + pub msg: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FrostKeyShare { + pub key_package: Vec, + pub pubkey_package: Vec, + pub verifying_key: Vec, +} + +pub async fn run_threshold_keygen( + mut tracer: Option<&mut dyn Tracer>, + i: u16, + t: u16, + n: u16, + role: ThresholdSignatureRoleType, + rng: &mut R, + party: M, +) -> Result> +where + R: RngCore + CryptoRng, + M: Mpc, + C: Ciphersuite, +{ + tracer.protocol_begins(); + + tracer.stage("Setup networking"); + let MpcParty { + delivery, runtime, .. + } = party.into_party(); + let (incomings, mut outgoings) = delivery.split(); + + let mut rounds = RoundsRouter::::builder(); + let round1 = rounds.add_round(RoundInput::::broadcast(i, n)); + let round2 = rounds.add_round(RoundInput::::p2p(i, n)); + let mut rounds = rounds.listen(incomings); + + // Round 1 + tracer.round_begins(); + tracer.stage("Compute round 1 dkg secret package"); + let (round1_secret_package, round1_package) = dkg_part1(i + 1, t, n, role, rng)?; + runtime.yield_now().await; + tracer.send_msg(); + outgoings + .send(Outgoing::broadcast(Msg::Round1(MsgRound1 { + msg: round1_package.serialize().unwrap_or_default(), + }))) + .await + .map_err(IoError::send_message)?; + tracer.msg_sent(); + + // Round 2 + tracer.round_begins(); + + tracer.receive_msgs(); + let round1_packages_map: BTreeMap, round1::Package> = rounds + .complete(round1) + .await + .map_err(IoError::receive_message)? + .into_iter_indexed() + .map(|(party_index, _, msg)| { + ( + (party_index + 1).try_into().expect("should be nonzero"), + round1::Package::deserialize(&msg.msg) + .unwrap_or_else(|_| panic!("Failed to deserialize round 1 package")), + ) + }) + .collect(); + tracer.msgs_received(); + tracer.stage("Compute round 2 dkg secret package"); + let (round2_secret_package, round2_packages_map) = + dkg_part2(role, round1_secret_package, &round1_packages_map)?; + + tracer.send_msg(); + for (receiver_identifier, round2_package) in round2_packages_map { + let receiver_index_bytes: Vec = receiver_identifier.serialize().as_ref().to_vec(); + let receiver_index: u16 = if receiver_index_bytes[0] == 0 && receiver_index_bytes[1] == 0 { + u16::from_le_bytes([ + receiver_index_bytes[receiver_index_bytes.len() - 1], + receiver_index_bytes[receiver_index_bytes.len() - 2], + ]) + } else { + u16::from_le_bytes([receiver_index_bytes[0], receiver_index_bytes[1]]) + }; + outgoings + .send(Outgoing::p2p( + receiver_index - 1, + Msg::Round2(MsgRound2 { + msg: round2_package.serialize().unwrap_or_default(), + }), + )) + .await + .map_err(IoError::send_message)?; + } + tracer.msg_sent(); + + // Round 3 + tracer.round_begins(); + + tracer.receive_msgs(); + let round2_packages: RoundMsgs = rounds + .complete(round2) + .await + .map_err(IoError::receive_message)?; + tracer.msgs_received(); + + tracer.stage("Compute round 3 dkg secret package"); + let round2_packages_map: BTreeMap, round2::Package> = round2_packages + .into_vec_including_me(MsgRound2 { msg: vec![] }) + .into_iter() + .enumerate() + .filter(|(inx, _)| *inx != i as usize) + .map(|(inx, msg)| { + let identifier = (inx as u16 + 1).try_into().expect("should be nonzero"); + let package = round2::Package::deserialize(&msg.msg) + .unwrap_or_else(|_| panic!("Failed to deserialize round 2 package")); + (identifier, package) + }) + .collect(); + let (key_package, pubkey_package) = dkg_part3( + role, + &round2_secret_package, + &round1_packages_map, + &round2_packages_map, + )?; + + tracer.protocol_ends(); + Ok(FrostKeyShare { + key_package: key_package.serialize().unwrap_or_default(), + pubkey_package: pubkey_package.serialize().unwrap_or_default(), + verifying_key: pubkey_package.verifying_key().serialize().as_ref().to_vec(), + }) +} + +fn validate_role(role: ThresholdSignatureRoleType) -> Result<(), Error> { + match role { + ThresholdSignatureRoleType::ZcashFrostEd25519 + | ThresholdSignatureRoleType::ZcashFrostEd448 + | ThresholdSignatureRoleType::ZcashFrostSecp256k1 + | ThresholdSignatureRoleType::ZcashFrostP256 + | ThresholdSignatureRoleType::ZcashFrostP384 + | ThresholdSignatureRoleType::ZcashFrostRistretto255 => {} + _ => Err(Error::InvalidFrostProtocol)?, + }; + + Ok(()) +} + +pub fn dkg_part1( + i: u16, + t: u16, + n: u16, + role: ThresholdSignatureRoleType, + rng: R, +) -> Result<(round1::SecretPackage, round1::Package), Error> +where + R: RngCore + CryptoRng, + C: Ciphersuite, +{ + validate_role::(role)?; + + let participant_identifier = i.try_into().expect("should be nonzero"); + Ok(frost_core::keys::dkg::part1::( + participant_identifier, + n, + t, + rng, + )?) +} + +#[allow(clippy::type_complexity)] +pub fn dkg_part2( + role: ThresholdSignatureRoleType, + secret_package: round1::SecretPackage, + round1_packages: &BTreeMap, round1::Package>, +) -> Result< + ( + round2::SecretPackage, + BTreeMap, round2::Package>, + ), + Error, +> +where + C: Ciphersuite, +{ + validate_role::(role)?; + + Ok(frost_core::keys::dkg::part2::( + secret_package, + round1_packages, + )?) +} + +pub fn dkg_part3( + role: ThresholdSignatureRoleType, + round2_secret_package: &round2::SecretPackage, + round1_packages: &BTreeMap, round1::Package>, + round2_packages: &BTreeMap, round2::Package>, +) -> Result<(KeyPackage, PublicKeyPackage), Error> +where + C: Ciphersuite, +{ + validate_role::(role)?; + + Ok(frost_core::keys::dkg::part3::( + round2_secret_package, + round1_packages, + round2_packages, + )?) +} diff --git a/protocols/zcash-frost/src/rounds/mod.rs b/protocols/zcash-frost/src/rounds/mod.rs new file mode 100644 index 000000000..d98533154 --- /dev/null +++ b/protocols/zcash-frost/src/rounds/mod.rs @@ -0,0 +1,70 @@ +use frost_core::Ciphersuite; +use round_based_21::rounds_router::{ + errors::{self as router_error, CompleteRoundError}, + simple_store::RoundInputError, +}; +use std::convert::Infallible; +use thiserror::Error; + +pub mod keygen; +pub mod sign; + +pub type BoxedError = Box; + +#[derive(Debug, Error)] +pub enum IoError { + #[error("send message")] + SendMessage(#[source] BoxedError), + #[error("receive message")] + ReceiveMessage(#[source] BoxedError), + #[error("got eof while recieving messages")] + ReceiveMessageEof, + #[error("route received message (possibly malicious behavior)")] + RouteReceivedError(router_error::CompleteRoundError), +} + +impl IoError { + pub fn send_message(err: E) -> Self { + Self::SendMessage(Box::new(err)) + } + + pub fn receive_message( + err: CompleteRoundError, + ) -> Self { + match err { + CompleteRoundError::Io(router_error::IoError::Io(e)) => { + Self::ReceiveMessage(Box::new(e)) + } + CompleteRoundError::Io(router_error::IoError::UnexpectedEof) => Self::ReceiveMessageEof, + + CompleteRoundError::ProcessMessage(e) => { + Self::RouteReceivedError(CompleteRoundError::ProcessMessage(e)) + } + CompleteRoundError::Other(e) => Self::RouteReceivedError(CompleteRoundError::Other(e)), + } + } +} + +#[derive(Debug, Error)] +pub enum Error { + #[error("i/o error")] + IoError(#[source] IoError), + #[error("unknown error")] + SerializationError, + #[error("Frost keygen error")] + FrostError(#[source] frost_core::Error), + #[error("Invalid frost protocol")] + InvalidFrostProtocol, +} + +impl From for Error { + fn from(e: IoError) -> Self { + Self::IoError(e) + } +} + +impl From> for Error { + fn from(e: frost_core::Error) -> Self { + Self::FrostError(e) + } +} diff --git a/protocols/zcash-frost/src/rounds/sign.rs b/protocols/zcash-frost/src/rounds/sign.rs new file mode 100644 index 000000000..8824767f8 --- /dev/null +++ b/protocols/zcash-frost/src/rounds/sign.rs @@ -0,0 +1,215 @@ +use dfns_cggmp21::progress::Tracer; +use frost_core::keys::{KeyPackage, PublicKeyPackage}; +use frost_core::round1::{SigningCommitments, SigningNonces}; +use frost_core::round2::{self, SignatureShare}; +use frost_core::{aggregate, round1, Ciphersuite, Field, Group, Identifier, SigningPackage}; +use futures::SinkExt; +use rand_core::{CryptoRng, RngCore}; +use round_based::rounds_router::simple_store::RoundInput; +use round_based_21 as round_based; + +use round_based::rounds_router::RoundsRouter; +use round_based::runtime::AsyncRuntime; +use round_based::ProtocolMessage; +use round_based::{Delivery, Mpc, MpcParty, Outgoing}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use tangle_primitives::roles::ThresholdSignatureRoleType; + +use super::{Error, IoError}; + +/// Message of threshold FROST signing protocol +#[derive(ProtocolMessage, Clone, Serialize, Deserialize)] +#[serde(bound = "")] +pub enum Msg { + /// Round 1 message + Round1(MsgRound1), + /// Round 2 message + Round2(MsgRound2), +} + +/// Message from round 1 +#[derive(Clone, Debug, Serialize, Deserialize, udigest::Digestable)] +#[serde(bound = "")] +#[udigest(bound = "")] +#[udigest(tag = "zcash.frost.sign.threshold.round1")] +pub struct MsgRound1 { + pub msg: Vec, +} +/// Message from round 2 +#[derive(Clone, Debug, Serialize, Deserialize, udigest::Digestable)] +#[serde(bound = "")] +#[udigest(bound = "")] +#[udigest(tag = "zcash.frost.sign.threshold.round2")] +pub struct MsgRound2 { + pub msg: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct FrostSignature { + pub group_signature: Vec, +} + +#[allow(clippy::too_many_arguments)] +pub async fn run_threshold_sign( + mut tracer: Option<&mut dyn Tracer>, + i: u16, + signers: Vec, + frost_keyshare: (KeyPackage, PublicKeyPackage), + message_to_sign: &[u8], + role: ThresholdSignatureRoleType, + rng: &mut R, + party: M, +) -> Result> +where + R: RngCore + CryptoRng, + M: Mpc, + C: Ciphersuite, +{ + tracer.protocol_begins(); + + tracer.stage("Setup networking"); + let MpcParty { + delivery, runtime, .. + } = party.into_party(); + let (incomings, mut outgoings) = delivery.split(); + let mut rounds = RoundsRouter::::builder(); + let round1 = rounds.add_round(RoundInput::::broadcast(i, signers.len() as u16)); + let round2 = rounds.add_round(RoundInput::::broadcast(i, signers.len() as u16)); + let mut rounds = rounds.listen(incomings); + + // Round 1 + tracer.round_begins(); + tracer.stage("Generate nonces and commitments for Round 1"); + let (nonces, commitments) = participant_round1(role, &frost_keyshare.0, rng)?; + runtime.yield_now().await; + tracer.send_msg(); + let my_round1_msg = MsgRound1 { + msg: commitments.serialize().unwrap_or_default(), + }; + outgoings + .send(Outgoing::broadcast(Msg::Round1(my_round1_msg.clone()))) + .await + .map_err(IoError::send_message)?; + tracer.msg_sent(); + + // Round 2 + tracer.round_begins(); + + tracer.receive_msgs(); + let round1_msgs: Vec = rounds + .complete(round1) + .await + .map_err(IoError::receive_message)? + .into_vec_including_me(my_round1_msg); + + let round1_signing_commitments = round1_msgs + .into_iter() + .enumerate() + .map(|(party_inx, msg)| { + let participant_identifier = Identifier::::try_from((party_inx + 1) as u16) + .expect("Failed to convert party index to identifier"); + let msg = SigningCommitments::::deserialize(&msg.msg) + .unwrap_or_else(|_| panic!("Failed to deserialize round 1 signing commitments")); + (participant_identifier, msg) + }) + .collect(); + tracer.msgs_received(); + + tracer.stage("Produce signature share using the Round 1 data"); + let signing_package = SigningPackage::::new(round1_signing_commitments, message_to_sign); + let signature_share: SignatureShare = + participant_round2(role, &signing_package, &nonces, &frost_keyshare.0)?; + runtime.yield_now().await; + tracer.send_msg(); + outgoings + .send(Outgoing::broadcast(Msg::Round2(MsgRound2 { + msg: signature_share.serialize().as_ref().to_vec(), + }))) + .await + .map_err(IoError::send_message)?; + tracer.msg_sent(); + + // Aggregation / output round + tracer.round_begins(); + + tracer.receive_msgs(); + let round2_signature_shares: BTreeMap, SignatureShare> = rounds + .complete(round2) + .await + .map_err(IoError::receive_message)? + .into_vec_including_me(MsgRound2 { + msg: signature_share.serialize().as_ref().to_vec(), + }) + .into_iter() + .enumerate() + .map(|(party_inx, msg)| { + let participant_identifier = Identifier::::try_from((party_inx + 1) as u16) + .expect("Failed to convert party index to identifier"); + let ser = <::Field as Field>::Serialization::try_from(msg.msg) + .map_err(|_e| Error::::SerializationError) + .expect("Failed to deserialize round 2 signature share"); + let sig_share = SignatureShare::::deserialize(ser) + .unwrap_or_else(|_| panic!("Failed to deserialize round 2 signature share")); + (participant_identifier, sig_share) + }) + .collect(); + tracer.msgs_received(); + + tracer.stage("Aggregate signature shares"); + let group_signature = aggregate( + &signing_package, + &round2_signature_shares, + &frost_keyshare.1, + )?; + + if frost_keyshare + .1 + .verifying_key() + .verify(message_to_sign, &group_signature) + .is_err() + { + return Err(frost_core::Error::::InvalidSignature.into()); + } else { + tracer.protocol_ends(); + } + + Ok(FrostSignature { + group_signature: group_signature.serialize().as_ref().to_vec(), + }) +} + +fn validate_role(role: ThresholdSignatureRoleType) -> Result<(), Error> { + match role { + ThresholdSignatureRoleType::ZcashFrostEd25519 + | ThresholdSignatureRoleType::ZcashFrostEd448 + | ThresholdSignatureRoleType::ZcashFrostSecp256k1 + | ThresholdSignatureRoleType::ZcashFrostP256 + | ThresholdSignatureRoleType::ZcashFrostP384 + | ThresholdSignatureRoleType::ZcashFrostRistretto255 => {} + _ => Err(Error::InvalidFrostProtocol)?, + }; + + Ok(()) +} + +/// Participant generates nonces and commitments for Round 1. +fn participant_round1( + role: ThresholdSignatureRoleType, + key_package: &KeyPackage, + rng: &mut R, +) -> Result<(SigningNonces, SigningCommitments), Error> { + validate_role::(role)?; + Ok(round1::commit(key_package.signing_share(), rng)) +} + +/// Participant produces their signature share using the `SigningPackage` and their `SigningNonces` from Round 1. +fn participant_round2( + role: ThresholdSignatureRoleType, + signing_package: &SigningPackage, + nonces: &SigningNonces, + key_package: &KeyPackage, +) -> Result, Error> { + validate_role::(role)?; + Ok(round2::sign(signing_package, nonces, key_package)?) +} diff --git a/protocols/zk-saas/src/lib.rs b/protocols/zk-saas/src/lib.rs index a214caa66..f57a09005 100644 --- a/protocols/zk-saas/src/lib.rs +++ b/protocols/zk-saas/src/lib.rs @@ -1,10 +1,6 @@ use crate::network::ZkNetworkService; use crate::protocol::ZkJobAdditionalParams; use async_trait::async_trait; -use gadget_common::client::{ - AccountId, ClientWithApi, JobsApiForGadget, JobsClient, PalletSubmitter, -}; -use gadget_common::debug_logger::DebugLogger; use gadget_common::full_protocol::SharedOptional; use gadget_common::prelude::*; use gadget_common::{generate_protocol, Error}; diff --git a/protocols/zk-saas/tests/zksaas.rs b/protocols/zk-saas/tests/zksaas.rs index 336e1f6c2..150fdbb61 100644 --- a/protocols/zk-saas/tests/zksaas.rs +++ b/protocols/zk-saas/tests/zksaas.rs @@ -20,9 +20,9 @@ mod tests { use std::str::FromStr; use std::sync::Arc; use tangle_primitives::jobs::{ - Groth16ProveRequest, Groth16System, HyperData, JobResult, JobSubmission, JobType, QAPShare, - ZkSaaSCircuitResult, ZkSaaSPhaseOneJobType, ZkSaaSPhaseTwoJobType, ZkSaaSPhaseTwoRequest, - ZkSaaSSystem, + FallbackOptions, Groth16ProveRequest, Groth16System, HyperData, JobResult, JobSubmission, + JobType, QAPShare, ZkSaaSCircuitResult, ZkSaaSPhaseOneJobType, ZkSaaSPhaseTwoJobType, + ZkSaaSPhaseTwoRequest, ZkSaaSSystem, }; use tangle_primitives::roles::{RoleType, ZeroKnowledgeRoleType}; use tangle_primitives::verifier::from_field_elements; @@ -74,6 +74,7 @@ mod tests { let num_inputs = matrices.num_instance_variables; let num_constraints = matrices.num_constraints; let phase1_submission = JobSubmission { + fallback: FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::ZkSaaSPhaseOne(ZkSaaSPhaseOneJobType { @@ -126,6 +127,7 @@ mod tests { let public_input = from_field_elements::(&[BigInt!("72587776472194017031617589674261467945970986113287823188107011979").into()]).unwrap().try_into().unwrap(); let phase_two_id = Jobs::next_job_id(); let phase2_submission = JobSubmission { + fallback: FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::ZkSaaSPhaseTwo(ZkSaaSPhaseTwoJobType { diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 8ca5977ef..f74c25335 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -108,6 +108,7 @@ macro_rules! generate_signing_and_keygen_tss_tests { let identities = (0..N).map(|i| id_to_sr25519_public(i as u8)).map(AccountId::from).collect::>(); let submission = JobSubmission { + fallback: gadget_common::prelude::FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::DKGTSSPhaseOne(DKGTSSPhaseOneJobType { @@ -143,6 +144,7 @@ macro_rules! generate_signing_and_keygen_tss_tests { let job_id = Jobs::next_job_id(); let identities = (0..N).map(|i| id_to_sr25519_public(i as u8)).map(AccountId::from).collect::>(); let submission = JobSubmission { + fallback: gadget_common::prelude::FallbackOptions::Destroy, expiry: 100, ttl: 100, job_type: JobType::DKGTSSPhaseTwo(DKGTSSPhaseTwoJobType { diff --git a/test-utils/src/mock.rs b/test-utils/src/mock.rs index 1122ca163..90772da1e 100644 --- a/test-utils/src/mock.rs +++ b/test-utils/src/mock.rs @@ -309,6 +309,18 @@ sp_api::mock_impl_runtime_apis! { Jobs::query_job_result(role_type, job_id) }) } + + fn query_next_job_id() -> JobId { + TEST_EXTERNALITIES.lock().as_ref().unwrap().execute_with(move || { + Jobs::query_next_job_id() + }) + } + + fn query_restaker_role_key(address: AccountId) -> Option> { + TEST_EXTERNALITIES.lock().as_ref().unwrap().execute_with(move || { + MockRolesHandler::get_validator_role_key(address) + }) + } } }