From 77bd576c0a563c9463ab21490443827f6e7e5821 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 19 Feb 2026 15:06:22 -0800 Subject: [PATCH] Add frost-taproot implementation --- .ackrc | 1 + .fdignore | 1 + .gitignore | 1 + AGENTS.md | 2 +- frost-taproot/Cargo.lock | 420 ++++++++++++++++++++++++++++++ frost-taproot/Cargo.toml | 5 + frost-taproot/src/commit.rs | 129 +++++++++ frost-taproot/src/context.rs | 78 ++++++ frost-taproot/src/ecc/group.rs | 95 +++++++ frost-taproot/src/ecc/hash.rs | 89 +++++++ frost-taproot/src/ecc/mod.rs | 4 + frost-taproot/src/ecc/state.rs | 46 ++++ frost-taproot/src/ecc/util.rs | 119 +++++++++ frost-taproot/src/ecdh.rs | 48 ++++ frost-taproot/src/group.rs | 40 +++ frost-taproot/src/helpers.rs | 103 ++++++++ frost-taproot/src/lib.rs | 35 +++ frost-taproot/src/poly.rs | 88 +++++++ frost-taproot/src/recover.rs | 86 ++++++ frost-taproot/src/refresh.rs | 47 ++++ frost-taproot/src/shares.rs | 103 ++++++++ frost-taproot/src/sign.rs | 141 ++++++++++ frost-taproot/src/types/commit.rs | 53 ++++ frost-taproot/src/types/ctx.rs | 58 +++++ frost-taproot/src/types/ecc.rs | 17 ++ frost-taproot/src/types/mod.rs | 11 + frost-taproot/src/types/share.rs | 39 +++ frost-taproot/src/types/sign.rs | 9 + frost-taproot/src/util/assert.rs | 56 ++++ frost-taproot/src/util/helpers.rs | 56 ++++ frost-taproot/src/util/mod.rs | 4 + frost-taproot/src/vss.rs | 55 ++++ 32 files changed, 2038 insertions(+), 1 deletion(-) create mode 100644 .ackrc create mode 100644 .fdignore create mode 100644 frost-taproot/Cargo.lock create mode 100644 frost-taproot/src/commit.rs create mode 100644 frost-taproot/src/context.rs create mode 100644 frost-taproot/src/ecc/group.rs create mode 100644 frost-taproot/src/ecc/hash.rs create mode 100644 frost-taproot/src/ecc/mod.rs create mode 100644 frost-taproot/src/ecc/state.rs create mode 100644 frost-taproot/src/ecc/util.rs create mode 100644 frost-taproot/src/ecdh.rs create mode 100644 frost-taproot/src/group.rs create mode 100644 frost-taproot/src/helpers.rs create mode 100644 frost-taproot/src/lib.rs create mode 100644 frost-taproot/src/poly.rs create mode 100644 frost-taproot/src/recover.rs create mode 100644 frost-taproot/src/refresh.rs create mode 100644 frost-taproot/src/shares.rs create mode 100644 frost-taproot/src/sign.rs create mode 100644 frost-taproot/src/types/commit.rs create mode 100644 frost-taproot/src/types/ctx.rs create mode 100644 frost-taproot/src/types/ecc.rs create mode 100644 frost-taproot/src/types/mod.rs create mode 100644 frost-taproot/src/types/share.rs create mode 100644 frost-taproot/src/types/sign.rs create mode 100644 frost-taproot/src/util/assert.rs create mode 100644 frost-taproot/src/util/helpers.rs create mode 100644 frost-taproot/src/util/mod.rs create mode 100644 frost-taproot/src/vss.rs diff --git a/.ackrc b/.ackrc new file mode 100644 index 0000000..3d920e9 --- /dev/null +++ b/.ackrc @@ -0,0 +1 @@ +--ignore-dir=target diff --git a/.fdignore b/.fdignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.fdignore @@ -0,0 +1 @@ +target diff --git a/.gitignore b/.gitignore index 6415016..31f3f95 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ ref +target diff --git a/AGENTS.md b/AGENTS.md index 48a756e..bda0100 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,6 @@ This repository is a monorepo with multiple narrowly-scoped packages intended to # frost-taproot -A rust implementation of BIP-340 FROST signatures, including DKG. It is compatible with cmdruid/frost typescript implementation, contained in the `ref` directory. +Frost taproot is a rust implementation of BIP-340 FROST signatures, including DKG. It is compatible with cmdruid/frost typescript implementation, contained in the `ref` directory. cmdruid/frost relies on noble/curves and noble/hashes, both of which are provided in the `ref` directory. However, the implementation for this project will rely on the k256 crate. diff --git a/frost-taproot/Cargo.lock b/frost-taproot/Cargo.lock new file mode 100644 index 0000000..fca95d3 --- /dev/null +++ b/frost-taproot/Cargo.lock @@ -0,0 +1,420 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "frost-taproot" +version = "0.1.0" +dependencies = [ + "k256", + "rand", + "sha2", + "signature", + "thiserror", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" diff --git a/frost-taproot/Cargo.toml b/frost-taproot/Cargo.toml index 34a4e89..d8a1100 100644 --- a/frost-taproot/Cargo.toml +++ b/frost-taproot/Cargo.toml @@ -4,3 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] +k256 = { version = "0.13", features = ["arithmetic", "hash2curve", "schnorr"] } +sha2 = "0.10" +rand = "0.8" +thiserror = "1" +signature = "2" diff --git a/frost-taproot/src/commit.rs b/frost-taproot/src/commit.rs new file mode 100644 index 0000000..05c1b28 --- /dev/null +++ b/frost-taproot/src/commit.rs @@ -0,0 +1,129 @@ +// Mirrors ref/frost/src/lib/commit.ts + +use crate::ecc::group::{scalar_multi, serialize_element, serialize_scalar_u32}; +use crate::ecc::hash::{h1, h4, h5}; +use crate::ecc::util::lift_x; +use crate::helpers::{generate_nonce, get_pubkey}; +use crate::types::{BindFactor, CommitmentPackage, PublicNonce, SecretShare}; +use crate::Error; + +/// Extract participant indices from a list of public nonces. +/// Mirrors `get_nonce_ids` in the TS implementation. +pub fn get_nonce_ids(pnonces: &[PublicNonce]) -> Vec { + pnonces.iter().map(|pn| pn.idx).collect() +} + +/// Encode all public nonces into a sorted byte prefix. +/// Mirrors `get_commits_prefix` in the TS implementation. +pub fn get_commits_prefix(pnonces: &[PublicNonce]) -> Vec { + let mut sorted = pnonces.to_vec(); + sorted.sort_by_key(|pn| pn.idx); + + let mut out = Vec::new(); + for pn in &sorted { + out.extend_from_slice(&serialize_scalar_u32(pn.idx)); + out.extend_from_slice(&pn.hidden_pn); + out.extend_from_slice(&pn.binder_pn); + } + out +} + +/// Build the group signing prefix: group_pk || H4(msg) || H5(commit_list). +/// Mirrors `get_group_prefix` in the TS implementation. +pub fn get_group_prefix(pnonces: &[PublicNonce], group_pk: &[u8; 33], message: &[u8]) -> Vec { + let msg_hash = h4(message); + let commit_list = get_commits_prefix(pnonces); + let commit_hash = h5(&commit_list); + + let mut out = Vec::new(); + out.extend_from_slice(group_pk); + out.extend_from_slice(&msg_hash); + out.extend_from_slice(&commit_hash); + out +} + +/// Look up the binding factor for a given participant index. +/// Mirrors `get_bind_factor` in the TS implementation. +pub fn get_bind_factor(binders: &[BindFactor], idx: u32) -> Result<[u8; 32], Error> { + binders + .iter() + .find(|b| b.idx == idx) + .map(|b| b.factor) + .ok_or(Error::RecordNotFound(idx)) +} + +/// Compute per-participant binding factors from the group prefix. +/// Mirrors `get_group_binders` in the TS implementation. +pub fn get_group_binders(pnonces: &[PublicNonce], prefix: &[u8]) -> Vec { + pnonces + .iter() + .map(|pn| { + let scalar_bytes = serialize_scalar_u32(pn.idx); + let mut rho_input = prefix.to_vec(); + rho_input.extend_from_slice(&scalar_bytes); + let factor = h1(&rho_input); + BindFactor { + idx: pn.idx, + factor, + } + }) + .collect() +} + +/// Compute the group public nonce: sum of (hidden_pn + bind_factor * binder_pn) for each participant. +/// Mirrors `get_group_pubnonce` in the TS implementation. +pub fn get_group_pubnonce( + pnonces: &[PublicNonce], + binders: &[BindFactor], +) -> Result<[u8; 33], Error> { + use crate::ecc::group::element_add; + use crate::ecc::util::scalar_from_bytes; + + let mut group_commit = None; + + for pn in pnonces { + let hidden_elem = lift_x(&pn.hidden_pn)?; + let binding_elem = lift_x(&pn.binder_pn)?; + let bind_factor_bytes = get_bind_factor(binders, pn.idx)?; + let bind_factor = scalar_from_bytes(&bind_factor_bytes); + let factored_elem = scalar_multi(&binding_elem, &bind_factor); + group_commit = Some(element_add(group_commit, Some(hidden_elem))?); + group_commit = Some(element_add(group_commit, Some(factored_elem))?); + } + + let pt = group_commit.ok_or(Error::BothPointsNull)?; + Ok(serialize_element(&pt)) +} + +/// Create a commitment package (secret + public nonces) for a signing session. +/// Mirrors `create_commit_pkg` in the TS implementation. +pub fn create_commit_pkg( + secret_share: &SecretShare, + hidden_seed: Option<&[u8; 32]>, + binder_seed: Option<&[u8; 32]>, +) -> CommitmentPackage { + let binder_sn = generate_nonce(&secret_share.seckey, binder_seed); + let hidden_sn = generate_nonce(&secret_share.seckey, hidden_seed); + let binder_pn = get_pubkey(&binder_sn); + let hidden_pn = get_pubkey(&hidden_sn); + CommitmentPackage { + idx: secret_share.idx, + binder_sn, + hidden_sn, + binder_pn, + hidden_pn, + } +} + +/// Find a commitment package for a given share. +/// Mirrors `get_commit_pkg` in the TS implementation. +pub fn get_commit_pkg( + commits: &[CommitmentPackage], + share: &SecretShare, +) -> Result { + commits + .iter() + .find(|c| c.idx == share.idx) + .cloned() + .ok_or(Error::RecordNotFound(share.idx)) +} diff --git a/frost-taproot/src/context.rs b/frost-taproot/src/context.rs new file mode 100644 index 0000000..d1e53cf --- /dev/null +++ b/frost-taproot/src/context.rs @@ -0,0 +1,78 @@ +// Mirrors ref/frost/src/lib/context.ts + +use crate::commit::{get_group_binders, get_group_prefix, get_group_pubnonce, get_nonce_ids}; +use crate::ecc::group::serialize_element; +use crate::ecc::state::get_point_state; +use crate::ecc::util::lift_x; +use crate::helpers::get_challenge; +use crate::types::{GroupCommitContext, GroupKeyContext, GroupSigningCtx, PublicNonce}; +use crate::Error; + +/// Build the group key context, applying optional tweaks. +/// Mirrors `get_group_key_context` in the TS implementation. +pub fn get_group_key_context(pubkey: &[u8], tweaks: &[[u8; 32]]) -> Result { + let int_pt = lift_x(pubkey)?; + let int_pk_bytes = serialize_element(&int_pt); + + let group_pt = get_point_state(int_pt, tweaks)?; + let group_pk = serialize_element(&group_pt.point); + + Ok(GroupKeyContext { + group_pt, + group_pk, + int_pt: Some(int_pt), + int_pk: Some(int_pk_bytes), + tweak: None, + }) +} + +/// Build the commit context from the key context, nonces, and message. +/// Mirrors `get_group_commit_context` in the TS implementation. +pub fn get_group_commit_context( + key_ctx: &GroupKeyContext, + pnonces: &[PublicNonce], + message: &[u8], +) -> Result { + let bind_prefix = get_group_prefix(pnonces, &key_ctx.group_pk, message); + let bind_factors = get_group_binders(pnonces, &bind_prefix); + let group_pn = get_group_pubnonce(pnonces, &bind_factors)?; + let indexes = get_nonce_ids(pnonces); + let challenge = get_challenge(&group_pn, &key_ctx.group_pk, message)?; + + Ok(GroupCommitContext { + bind_factors, + bind_prefix, + challenge, + group_pn, + indexes, + message: message.to_vec(), + pnonces: pnonces.to_vec(), + }) +} + +/// Build the full signing context. +/// Mirrors `get_group_signing_ctx` in the TS implementation. +pub fn get_group_signing_ctx( + group_pk: &[u8], + pnonces: &[PublicNonce], + message: &[u8], + tweaks: &[[u8; 32]], +) -> Result { + let key_ctx = get_group_key_context(group_pk, tweaks)?; + let com_ctx = get_group_commit_context(&key_ctx, pnonces, message)?; + + Ok(GroupSigningCtx { + group_pt: key_ctx.group_pt, + group_pk: key_ctx.group_pk, + int_pt: key_ctx.int_pt, + int_pk: key_ctx.int_pk, + tweak: key_ctx.tweak, + bind_factors: com_ctx.bind_factors, + bind_prefix: com_ctx.bind_prefix, + challenge: com_ctx.challenge, + group_pn: com_ctx.group_pn, + indexes: com_ctx.indexes, + message: com_ctx.message, + pnonces: com_ctx.pnonces, + }) +} diff --git a/frost-taproot/src/ecc/group.rs b/frost-taproot/src/ecc/group.rs new file mode 100644 index 0000000..2a6cad4 --- /dev/null +++ b/frost-taproot/src/ecc/group.rs @@ -0,0 +1,95 @@ +// Mirrors ref/frost/src/ecc/group.ts +// Group operations on secp256k1 using k256. + +use k256::{ProjectivePoint, Scalar}; +use rand::rngs::OsRng; +use rand::RngCore; + +use super::util::{deserialize_point, mod_n, scalar_from_bytes, scalar_to_bytes, serialize_point}; +use crate::Error; +use k256::U256; + +/// The group order N. +pub fn order() -> Scalar { + // N as a scalar is zero (it wraps), so we expose the U256 constant from util. + // Callers needing N for negation use `Scalar::ZERO - Scalar::ONE` + 1 = N-1 pattern, + // or use scalar_neg directly. + Scalar::ZERO +} + +/// The identity (point at infinity). +pub fn identity() -> ProjectivePoint { + ProjectivePoint::IDENTITY +} + +/// Generate a random scalar in [1, N-1]. +pub fn random_scalar() -> Scalar { + let mut bytes = [0u8; 32]; + OsRng.fill_bytes(&mut bytes); + mod_n(U256::from_be_slice(&bytes)) +} + +/// Add two points. Either may be the identity (None-equivalent in TS was null). +pub fn element_add( + a: Option, + b: Option, +) -> Result { + match (a, b) { + (None, None) => Err(Error::BothPointsNull), + (None, Some(b)) => Ok(b), + (Some(a), None) => Ok(a), + (Some(a), Some(b)) => Ok(a + b), + } +} + +/// Sum a slice of optional points. +pub fn element_add_many(elems: &[Option]) -> Result { + if elems.is_empty() { + return Err(Error::BothPointsNull); + } + let mut acc = elems[0]; + for &e in &elems[1..] { + acc = Some(element_add(acc, e)?); + } + acc.ok_or(Error::BothPointsNull) +} + +/// Scalar multiplication: k * A. +pub fn scalar_multi(a: &ProjectivePoint, k: &Scalar) -> ProjectivePoint { + a * k +} + +/// Scalar base multiplication: k * G. +pub fn scalar_base_multi(k: &Scalar) -> ProjectivePoint { + ProjectivePoint::GENERATOR * k +} + +/// Serialize a point to 33 compressed bytes. +pub fn serialize_element(pt: &ProjectivePoint) -> [u8; 33] { + serialize_point(pt) +} + +/// Deserialize a point from 33 compressed bytes. +pub fn deserialize_element(bytes: &[u8; 33]) -> Result { + deserialize_point(bytes) +} + +/// Serialize a scalar index (u32) as a 32-byte big-endian value. +/// Mirrors `G.SerializeScalar(idx)` where idx is a small integer. +pub fn serialize_scalar_u32(idx: u32) -> [u8; 32] { + let mut out = [0u8; 32]; + out[28..].copy_from_slice(&idx.to_be_bytes()); + out +} + +/// Serialize a Scalar to 32 bytes. +pub fn serialize_scalar(s: &Scalar) -> [u8; 32] { + scalar_to_bytes(s) +} + +/// Deserialize 32 bytes into a Scalar (reduced mod N). +pub fn deserialize_scalar(bytes: &[u8; 32]) -> Scalar { + scalar_from_bytes(bytes) +} + +pub use super::util::{has_even_y, lift_x, negate_point, pow_n}; diff --git a/frost-taproot/src/ecc/hash.rs b/frost-taproot/src/ecc/hash.rs new file mode 100644 index 0000000..03e2a14 --- /dev/null +++ b/frost-taproot/src/ecc/hash.rs @@ -0,0 +1,89 @@ +// Mirrors ref/frost/src/ecc/hash.ts +// FROST-secp256k1-SHA256-v1 hash functions H1–H5. +// Spec: draft-irtf-cfrg-frost-15, section 6.5. + +use k256::{ + elliptic_curve::hash2curve::{ExpandMsg, ExpandMsgXmd, Expander}, + Scalar, +}; +use sha2::{Digest, Sha256}; + +use super::util::mod_n; +use k256::U256; + +const DOMAIN: &str = "FROST-secp256k1-SHA256-v1"; + +/// Build the DST for a given sub-tag. +fn dst(sub: &str) -> Vec { + format!("{}{}", DOMAIN, sub).into_bytes() +} + +/// hash_to_field using XMD with SHA-256, outputting one field element mod N. +/// Mirrors `hash_to_field(msg, 1, { m:1, p:N, k:128, expand:'xmd', hash:sha256, DST })`. +fn hash_to_field_n(msg: &[u8], dst_bytes: &[u8]) -> Scalar { + // expand_message_xmd with len_in_bytes = 48 (ceil((log2(N)+k)/8) = ceil((256+128)/8) = 48) + let mut uniform = [0u8; 48]; + ExpandMsgXmd::::expand_message(&[msg], &[dst_bytes], 48) + .expect("expand_message failed") + .fill_bytes(&mut uniform); + + // Interpret as a 384-bit integer mod N. + // We take the low 256 bits after reduction — use U256 from the 48-byte big-endian value. + // Noble does: interpret as big-endian integer, then mod N. + // We replicate by taking the full 48 bytes as a big integer mod N. + // k256 U256 is only 256 bits, so we do two-step reduction: + // high 16 bytes * 2^256 + low 32 bytes, all mod N. + let (hi, lo) = uniform.split_at(16); + let mut hi32 = [0u8; 32]; + hi32[16..].copy_from_slice(hi); + let mut lo32 = [0u8; 32]; + lo32.copy_from_slice(lo); + + let hi_scalar = mod_n(U256::from_be_slice(&hi32)); + let lo_scalar = mod_n(U256::from_be_slice(&lo32)); + + // 2^128 mod N as a scalar + let shift = { + let mut s = [0u8; 32]; + s[16] = 1; // 2^128 + mod_n(U256::from_be_slice(&s)) + }; + + hi_scalar * shift + lo_scalar +} + +/// H1: rho — binding factor hash. +pub fn h1(msg: &[u8]) -> [u8; 32] { + let s = hash_to_field_n(msg, &dst("rho")); + s.to_bytes().into() +} + +/// H2: chal — challenge hash. +pub fn h2(msg: &[u8]) -> [u8; 32] { + let s = hash_to_field_n(msg, &dst("chal")); + s.to_bytes().into() +} + +/// H3: nonce — nonce generation hash. +pub fn h3(msg: &[u8]) -> [u8; 32] { + let s = hash_to_field_n(msg, &dst("nonce")); + s.to_bytes().into() +} + +/// H4: msg — plain SHA-256 with domain prefix. +pub fn h4(msg: &[u8]) -> [u8; 32] { + let prefix = dst("msg"); + let mut hasher = Sha256::new(); + hasher.update(&prefix); + hasher.update(msg); + hasher.finalize().into() +} + +/// H5: com — plain SHA-256 with domain prefix. +pub fn h5(msg: &[u8]) -> [u8; 32] { + let prefix = dst("com"); + let mut hasher = Sha256::new(); + hasher.update(&prefix); + hasher.update(msg); + hasher.finalize().into() +} diff --git a/frost-taproot/src/ecc/mod.rs b/frost-taproot/src/ecc/mod.rs new file mode 100644 index 0000000..e51319f --- /dev/null +++ b/frost-taproot/src/ecc/mod.rs @@ -0,0 +1,4 @@ +pub mod group; +pub mod hash; +pub mod state; +pub mod util; diff --git a/frost-taproot/src/ecc/state.rs b/frost-taproot/src/ecc/state.rs new file mode 100644 index 0000000..ebc492c --- /dev/null +++ b/frost-taproot/src/ecc/state.rs @@ -0,0 +1,46 @@ +// Mirrors ref/frost/src/ecc/state.ts + +use k256::{ProjectivePoint, Scalar}; + +use super::group::{element_add, scalar_base_multi}; +use super::util::{has_even_y, mod_n, negate_point, scalar_from_bytes}; +use crate::types::PointState; +use crate::Error; +use k256::U256; + +/// Computes the accumulative parity state for a given point with optional key tweaks. +/// Mirrors `get_point_state` in the TS implementation. +pub fn get_point_state(element: ProjectivePoint, tweaks: &[[u8; 32]]) -> Result { + let pos = Scalar::ONE; + let neg = -pos; // N - 1 mod N + + let mut point = element; + let mut parity; + let mut state = pos; + let mut tweak = Scalar::ZERO; + + for t_bytes in tweaks { + let t = scalar_from_bytes(t_bytes); + let tg = scalar_base_multi(&t); + + parity = if has_even_y(&point) { pos } else { neg }; + + if parity == neg { + point = negate_point(&point); + } + + point = element_add(Some(point), Some(tg))?; + + state = state * parity; + tweak = mod_n(U256::from_be_slice(&(t + parity * tweak).to_bytes())); + } + + parity = if has_even_y(&point) { pos } else { neg }; + + Ok(PointState { + parity, + point, + state, + tweak, + }) +} diff --git a/frost-taproot/src/ecc/util.rs b/frost-taproot/src/ecc/util.rs new file mode 100644 index 0000000..553f03e --- /dev/null +++ b/frost-taproot/src/ecc/util.rs @@ -0,0 +1,119 @@ +// Mirrors ref/frost/src/ecc/util.ts + +use std::ops::Neg; + +use k256::{ + elliptic_curve::{ + ops::Reduce, + sec1::{FromEncodedPoint, ToEncodedPoint}, + }, + EncodedPoint, ProjectivePoint, Scalar, U256, +}; + +use crate::Error; + +/// Secp256k1 field prime P. +pub const P: U256 = + U256::from_be_hex("fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f"); + +/// Secp256k1 group order N. +pub const N: U256 = + U256::from_be_hex("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141"); + +/// Reduce a 256-bit integer mod N into a Scalar. +pub fn mod_n(x: U256) -> Scalar { + >::reduce(x) +} + +/// Scalar from raw bytes, reduced mod N. +pub fn scalar_from_bytes(bytes: &[u8; 32]) -> Scalar { + mod_n(U256::from_be_slice(bytes)) +} + +/// Scalar to big-endian bytes. +pub fn scalar_to_bytes(s: &Scalar) -> [u8; 32] { + s.to_bytes().into() +} + +/// Add two scalars mod N. +pub fn scalar_add(a: Scalar, b: Scalar) -> Scalar { + a + b +} + +/// Multiply two scalars mod N. +pub fn scalar_mul(a: Scalar, b: Scalar) -> Scalar { + a * b +} + +/// Negate a scalar mod N. +pub fn scalar_neg(a: Scalar) -> Scalar { + -a +} + +/// Modular inverse of a scalar (panics if zero). +pub fn scalar_invert(a: &Scalar) -> Result { + Option::from(a.invert()).ok_or(Error::ScalarInversion) +} + +/// Compute base^exp mod N using repeated squaring. +/// Mirrors `pow_n` in the TS implementation. +pub fn pow_n(base: u64, exp: u64) -> Scalar { + if exp == 0 { + return Scalar::ONE; + } + let mut result = Scalar::ONE; + let mut b = mod_n(U256::from(base)); + let mut e = exp; + while e > 0 { + if e & 1 == 1 { + result = result * b; + } + b = b * b; + e >>= 1; + } + result +} + +/// Lift an x-only (32-byte) or compressed (33-byte) pubkey to a ProjectivePoint. +/// Mirrors `lift_x` in the TS implementation — always returns the even-Y variant. +pub fn lift_x(bytes: &[u8]) -> Result { + let encoded = match bytes.len() { + 32 => { + // Prepend 0x02 (even parity) to get a compressed point. + let mut buf = [0u8; 33]; + buf[0] = 0x02; + buf[1..].copy_from_slice(bytes); + EncodedPoint::from_bytes(&buf).map_err(|_| Error::InvalidPoint)? + } + 33 => EncodedPoint::from_bytes(bytes).map_err(|_| Error::InvalidPoint)?, + _ => return Err(Error::InvalidPoint), + }; + let pt = ProjectivePoint::from_encoded_point(&encoded); + Option::from(pt).ok_or(Error::InvalidPoint) +} + +/// Serialize a ProjectivePoint to a 33-byte compressed encoding. +pub fn serialize_point(pt: &ProjectivePoint) -> [u8; 33] { + let encoded = pt.to_encoded_point(true); + let bytes = encoded.as_bytes(); + let mut out = [0u8; 33]; + out.copy_from_slice(bytes); + out +} + +/// Deserialize a 33-byte compressed point. +pub fn deserialize_point(bytes: &[u8; 33]) -> Result { + let encoded = EncodedPoint::from_bytes(bytes as &[u8]).map_err(|_| Error::InvalidPoint)?; + Option::from(ProjectivePoint::from_encoded_point(&encoded)).ok_or(Error::InvalidPoint) +} + +/// Returns true if the point has an even Y coordinate. +pub fn has_even_y(pt: &ProjectivePoint) -> bool { + let encoded = pt.to_encoded_point(true); + encoded.as_bytes()[0] == 0x02 +} + +/// Negate a point (flip Y). +pub fn negate_point(pt: &ProjectivePoint) -> ProjectivePoint { + Neg::neg(*pt) +} diff --git a/frost-taproot/src/ecdh.rs b/frost-taproot/src/ecdh.rs new file mode 100644 index 0000000..46c1e1c --- /dev/null +++ b/frost-taproot/src/ecdh.rs @@ -0,0 +1,48 @@ +// Mirrors ref/frost/src/lib/ecdh.ts + +use k256::Scalar; + +use crate::ecc::group::{element_add, scalar_multi, serialize_element}; +use crate::ecc::util::{lift_x, scalar_from_bytes}; +use crate::poly::{calc_lagrange_coeff, index_to_scalar}; +use crate::types::{PublicShare, SecretShare}; +use crate::Error; + +/// Compute an ECDH share: a participant's contribution to a shared secret. +/// Mirrors `create_ecdh_share` in the TS implementation. +pub fn create_ecdh_share( + members: &[u32], + share: &SecretShare, + pubkey: &[u8; 33], +) -> Result { + let mbrs: Vec = members + .iter() + .filter(|&&idx| idx != share.idx) + .map(|&idx| index_to_scalar(idx)) + .collect(); + + let idx = index_to_scalar(share.idx); + let secret = scalar_from_bytes(&share.seckey); + let point = lift_x(pubkey)?; + + let l_coeff = calc_lagrange_coeff(&mbrs, idx, Scalar::ZERO)?; + let p_coeff = l_coeff * secret; + let ecdh_pt = scalar_multi(&point, &p_coeff); + + Ok(PublicShare { + idx: share.idx, + pubkey: serialize_element(&ecdh_pt), + }) +} + +/// Derive the shared ECDH secret by summing all participant ECDH shares. +/// Mirrors `derive_ecdh_secret` in the TS implementation. +pub fn derive_ecdh_secret(shares: &[PublicShare]) -> Result<[u8; 33], Error> { + let mut point = None; + for share in shares { + let pt = lift_x(&share.pubkey)?; + point = Some(element_add(point, Some(pt))?); + } + let pt = point.ok_or(Error::BothPointsNull)?; + Ok(serialize_element(&pt)) +} diff --git a/frost-taproot/src/group.rs b/frost-taproot/src/group.rs new file mode 100644 index 0000000..d358391 --- /dev/null +++ b/frost-taproot/src/group.rs @@ -0,0 +1,40 @@ +// Mirrors ref/frost/src/lib/group.ts +// High-level share set creation (trusted dealer). + +use crate::shares::create_shares; +use crate::types::{DealerShareSet, SecretShareSet}; +use crate::vss::{create_share_coeffs, get_share_commits}; +use crate::Error; + +/// Create a set of secret shares and VSS commitments. +/// Mirrors `create_share_set` in the TS implementation. +pub fn create_share_set( + threshold: usize, + share_max: u32, + secrets: &[[u8; 32]], +) -> Result { + let coeffs = create_share_coeffs(secrets, threshold); + let shares = create_shares(&coeffs, share_max)?; + let vss_commits = get_share_commits(&coeffs); + Ok(SecretShareSet { + shares, + vss_commits, + }) +} + +/// Create a dealer share set that also exposes the group public key. +/// The group public key is the first VSS commitment (the constant term's public key). +/// Mirrors `create_dealer_set` in the TS implementation. +pub fn create_dealer_set( + threshold: usize, + share_max: u32, + secrets: &[[u8; 32]], +) -> Result { + let share_set = create_share_set(threshold, share_max, secrets)?; + let group_pk = share_set.vss_commits[0]; + Ok(DealerShareSet { + shares: share_set.shares, + vss_commits: share_set.vss_commits, + group_pk, + }) +} diff --git a/frost-taproot/src/helpers.rs b/frost-taproot/src/helpers.rs new file mode 100644 index 0000000..3de9625 --- /dev/null +++ b/frost-taproot/src/helpers.rs @@ -0,0 +1,103 @@ +// Mirrors ref/frost/src/lib/helpers.ts + +use k256::Scalar; + +use crate::ecc::group::{scalar_base_multi, scalar_multi, serialize_element}; +use crate::ecc::hash::h3; +use crate::ecc::util::{lift_x, mod_n, scalar_from_bytes, scalar_to_bytes}; +use crate::util::assert; +use crate::util::helpers::{hash340, random_bytes_32}; +use crate::Error; +use k256::U256; + +/// Generate a secret key by hashing optional auxiliary bytes through H3. +/// Mirrors `generate_seckey` in the TS implementation. +pub fn generate_seckey(aux: Option<&[u8; 32]>) -> [u8; 32] { + let aux_bytes = match aux { + Some(a) => *a, + None => random_bytes_32(), + }; + h3(&aux_bytes) +} + +/// Generate a secret nonce by hashing (aux || secret) through H3. +/// Mirrors `generate_nonce` in the TS implementation. +pub fn generate_nonce(secret: &[u8; 32], aux_seed: Option<&[u8; 32]>) -> [u8; 32] { + let aux = match aux_seed { + Some(a) => *a, + None => random_bytes_32(), + }; + let mut input = [0u8; 64]; + input[..32].copy_from_slice(&aux); + input[32..].copy_from_slice(secret); + h3(&input) +} + +/// Derive a compressed public key from a secret key. +/// Mirrors `get_pubkey` in the TS implementation. +pub fn get_pubkey(secret: &[u8; 32]) -> [u8; 33] { + let scalar = scalar_from_bytes(secret); + let point = scalar_base_multi(&scalar); + serialize_element(&point) +} + +/// Tweak a secret key by multiplying by a tweak scalar. +/// Mirrors `tweak_seckey` in the TS implementation. +pub fn tweak_seckey(seckey: &[u8; 32], tweak: &[u8; 32]) -> [u8; 32] { + let coeff = scalar_from_bytes(tweak); + let secret = scalar_from_bytes(seckey); + scalar_to_bytes(&(secret * coeff)) +} + +/// Tweak a public key by multiplying by a tweak scalar. +/// Mirrors `tweak_pubkey` in the TS implementation. +pub fn tweak_pubkey(pubkey: &[u8], tweak: &[u8; 32]) -> Result<[u8; 33], Error> { + let coeff = scalar_from_bytes(tweak); + let point = lift_x(pubkey)?; + let tweaked = scalar_multi(&point, &coeff); + Ok(serialize_element(&tweaked)) +} + +/// Compute a BIP340-compatible challenge hash. +/// challenge = SHA256(SHA256("BIP0340/challenge") || SHA256("BIP0340/challenge") || R_x || P_x || msg) +/// Mirrors `get_challenge` in the TS implementation. +pub fn get_challenge(pnonce: &[u8], pubkey: &[u8], message: &[u8]) -> Result { + let grp_pn = convert_pubkey_to_bip340(pnonce)?; + let grp_pk = convert_pubkey_to_bip340(pubkey)?; + assert::ok( + grp_pn.len() == 32, + "pnonce must be 32 bytes after conversion", + )?; + assert::ok( + grp_pk.len() == 32, + "pubkey must be 32 bytes after conversion", + )?; + let digest = hash340("BIP0340/challenge", &[&grp_pn, &grp_pk, message]); + Ok(mod_n(U256::from_be_slice(&digest))) +} + +/// Convert a pubkey to BIP340 format (x-only, 32 bytes). +/// If 33 bytes (compressed), strip the prefix byte. +/// If 32 bytes, return as-is. +pub fn convert_pubkey_to_bip340(pubkey: &[u8]) -> Result, Error> { + match pubkey.len() { + 33 => Ok(pubkey[1..].to_vec()), + 32 => Ok(pubkey.to_vec()), + _ => Err(Error::InvalidPoint), + } +} + +/// Convert a pubkey to ECDSA format (compressed, 33 bytes). +/// If 32 bytes (x-only), prepend 0x02. +/// If 33 bytes, return as-is. +pub fn convert_pubkey_to_ecdsa(pubkey: &[u8]) -> Result, Error> { + match pubkey.len() { + 32 => { + let mut out = vec![0x02u8]; + out.extend_from_slice(pubkey); + Ok(out) + } + 33 => Ok(pubkey.to_vec()), + _ => Err(Error::InvalidPoint), + } +} diff --git a/frost-taproot/src/lib.rs b/frost-taproot/src/lib.rs new file mode 100644 index 0000000..4c9820e --- /dev/null +++ b/frost-taproot/src/lib.rs @@ -0,0 +1,35 @@ +pub mod ecc; +pub mod types; +pub mod util; + +pub mod commit; +pub mod context; +pub mod ecdh; +pub mod group; +pub mod helpers; +pub mod poly; +pub mod recover; +pub mod refresh; +pub mod shares; +pub mod sign; +pub mod vss; + +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum Error { + #[error("assertion failed: {0}")] + Assertion(String), + + #[error("invalid point encoding")] + InvalidPoint, + + #[error("both points are null")] + BothPointsNull, + + #[error("scalar inversion failed (zero scalar)")] + ScalarInversion, + + #[error("record not found for index: {0}")] + RecordNotFound(u32), +} diff --git a/frost-taproot/src/poly.rs b/frost-taproot/src/poly.rs new file mode 100644 index 0000000..576d9b7 --- /dev/null +++ b/frost-taproot/src/poly.rs @@ -0,0 +1,88 @@ +// Mirrors ref/frost/src/lib/poly.ts +// Polynomial evaluation and Lagrange interpolation over the secp256k1 scalar field. + +use k256::Scalar; + +use crate::ecc::util::{scalar_from_bytes, scalar_invert}; +use crate::util::assert; +use crate::Error; + +/// Evaluate a polynomial at x using Horner's method. +/// Coefficients are in ascending order: coeffs[0] + coeffs[1]*x + coeffs[2]*x^2 + ... +/// Mirrors `evaluate_x` in the TS implementation. +pub fn evaluate_x(coeffs: &[Scalar], x: Scalar) -> Result { + if x == Scalar::ZERO { + return Err(Error::Assertion("x is zero".to_string())); + } + + let mut value = Scalar::ZERO; + for coeff in coeffs.iter().rev() { + value = value * x + coeff; + } + Ok(value) +} + +/// Interpolate a polynomial at x=0 (the root) using Lagrange interpolation. +/// Points are (x, y) pairs of Scalars. +/// Mirrors `interpolate_root` in the TS implementation. +pub fn interpolate_root(points: &[(Scalar, Scalar)]) -> Result { + let xs: Vec = points.iter().map(|(x, _)| *x).collect(); + let mut p = Scalar::ZERO; + for (x, y) in points { + let delta = interpolate_x(&xs, *x)?; + p = p + delta * y; + } + Ok(p) +} + +/// Compute the Lagrange basis polynomial value at x=0 for the given x-coordinate, +/// relative to the set of x-coordinates L. +/// Mirrors `interpolate_x` in the TS implementation. +pub fn interpolate_x(l: &[Scalar], x: Scalar) -> Result { + assert::is_included(l, &x)?; + assert::is_unique_set(l)?; + + let mut numerator = Scalar::ONE; + let mut denominator = Scalar::ONE; + + for &x_j in l { + if x_j == x { + continue; + } + numerator = numerator * x_j; + denominator = denominator * (x_j + (-x)); // x_j - x + } + + let inv = scalar_invert(&denominator)?; + Ok(numerator * inv) +} + +/// Compute the Lagrange coefficient for participant P relative to members L, +/// evaluated at point x. +/// Mirrors `calc_lagrange_coeff` in the TS implementation. +pub fn calc_lagrange_coeff(l: &[Scalar], p: Scalar, x: Scalar) -> Result { + assert::is_unique_set(l)?; + + let mut numerator = Scalar::ONE; + let mut denominator = Scalar::ONE; + + for &x_j in l { + if x_j == p { + continue; + } + numerator = numerator * (x + (-x_j)); // x - x_j + denominator = denominator * (p + (-x_j)); // p - x_j + } + + let inv = scalar_invert(&denominator)?; + Ok(numerator * inv) +} + +/// Convert a u32 participant index to a Scalar. +pub fn index_to_scalar(idx: u32) -> Scalar { + scalar_from_bytes(&{ + let mut b = [0u8; 32]; + b[28..].copy_from_slice(&idx.to_be_bytes()); + b + }) +} diff --git a/frost-taproot/src/recover.rs b/frost-taproot/src/recover.rs new file mode 100644 index 0000000..4b201da --- /dev/null +++ b/frost-taproot/src/recover.rs @@ -0,0 +1,86 @@ +// Mirrors ref/frost/src/lib/recover.ts + +use k256::Scalar; + +use crate::ecc::util::{scalar_from_bytes, scalar_to_bytes}; +use crate::poly::{calc_lagrange_coeff, index_to_scalar}; +use crate::types::{SecretShare, SecretSharePackage}; +use crate::util::assert; +use crate::vss::{create_share_coeffs, get_share_commits}; +use crate::Error; + +/// Generate recovery shares that allow a target participant to reconstruct their share. +/// Mirrors `gen_recovery_shares` in the TS implementation. +pub fn gen_recovery_shares( + members: &[u32], + share: &SecretShare, + target: u32, + threshold: usize, + secrets: &[[u8; 32]], +) -> Result { + assert::ok( + members.len() >= threshold, + "not enough members to meet threshold", + )?; + + let mut sorted_members = members.to_vec(); + sorted_members.sort(); + + let share_idx = index_to_scalar(share.idx); + let target_idx = index_to_scalar(target); + + // Members excluding the current share holder. + let mbrs: Vec = sorted_members + .iter() + .filter(|&&idx| idx != share.idx) + .map(|&idx| index_to_scalar(idx)) + .collect(); + + let share_seckey = scalar_from_bytes(&share.seckey); + let lgrng_coeff = calc_lagrange_coeff(&mbrs, share_idx, target_idx)?; + + assert::ok( + lgrng_coeff != Scalar::ZERO, + "lagrange coefficient must be greater than zero", + )?; + + let rand_coeffs = create_share_coeffs(secrets, threshold - 1); + let coeff_sum = rand_coeffs.iter().fold(Scalar::ZERO, |p, n| p + n); + let repair_coeff = lgrng_coeff * share_seckey - coeff_sum; + + let repair_shares: Vec = rand_coeffs + .iter() + .cloned() + .chain(std::iter::once(repair_coeff)) + .collect(); + + let vss_commits = get_share_commits(&repair_shares); + + let shares: Vec = sorted_members + .iter() + .enumerate() + .map(|(i, &idx)| SecretShare { + idx, + seckey: scalar_to_bytes(&repair_shares[i]), + }) + .collect(); + + Ok(SecretSharePackage { + idx: share.idx, + vss_commits, + shares, + }) +} + +/// Recover a participant's share by summing aggregated recovery shares. +/// Mirrors `recover_share` in the TS implementation. +pub fn recover_share(shares: &[SecretShare], idx: u32) -> SecretShare { + let summed = shares + .iter() + .map(|s| scalar_from_bytes(&s.seckey)) + .fold(Scalar::ZERO, |p, n| p + n); + SecretShare { + idx, + seckey: scalar_to_bytes(&summed), + } +} diff --git a/frost-taproot/src/refresh.rs b/frost-taproot/src/refresh.rs new file mode 100644 index 0000000..2006ada --- /dev/null +++ b/frost-taproot/src/refresh.rs @@ -0,0 +1,47 @@ +// Mirrors ref/frost/src/lib/refresh.ts + +use k256::Scalar; + +use crate::shares::{combine_set, create_shares}; +use crate::types::{SecretShare, SecretSharePackage}; +use crate::vss::{create_share_coeffs, get_share_commits}; +use crate::Error; + +/// Generate refresh shares for proactive secret sharing. +/// The polynomial has a zero constant term, so adding these shares to existing +/// shares does not change the underlying secret. +/// Mirrors `gen_refresh_shares` in the TS implementation. +pub fn gen_refresh_shares( + index: u32, + threshold: usize, + share_max: u32, + secrets: &[[u8; 32]], +) -> Result { + // Auxiliary coefficients (threshold - 1 of them, no constant term). + let sub_coeffs = create_share_coeffs(secrets, threshold - 1); + // Prepend zero as the constant term so the polynomial evaluates to 0 at x=0. + let coeffs: Vec = std::iter::once(Scalar::ZERO) + .chain(sub_coeffs.iter().cloned()) + .collect(); + + let shares = create_shares(&coeffs, share_max)?; + let vss_commits = get_share_commits(&sub_coeffs); + + Ok(SecretSharePackage { + idx: index, + vss_commits, + shares, + }) +} + +/// Apply refresh shares to a current share by summing them. +/// Mirrors `refresh_share` in the TS implementation. +pub fn refresh_share( + refresh_shares: &[SecretShare], + current_share: &SecretShare, +) -> Result { + let all: Vec = std::iter::once(current_share.clone()) + .chain(refresh_shares.iter().cloned()) + .collect(); + combine_set(&all) +} diff --git a/frost-taproot/src/shares.rs b/frost-taproot/src/shares.rs new file mode 100644 index 0000000..cf4a618 --- /dev/null +++ b/frost-taproot/src/shares.rs @@ -0,0 +1,103 @@ +// Mirrors ref/frost/src/lib/shares.ts + +use k256::{elliptic_curve::point::AffineCoordinates, Scalar}; + +use crate::ecc::group::{scalar_base_multi, scalar_multi}; +use crate::ecc::util::{lift_x, pow_n, scalar_from_bytes, scalar_to_bytes}; +use crate::poly::{evaluate_x, index_to_scalar, interpolate_root}; +use crate::types::SecretShare; +use crate::util::assert; +use crate::Error; + +/// Create secret shares by evaluating the polynomial at indices 1..=count. +/// Mirrors `create_shares` in the TS implementation. +pub fn create_shares(coeffs: &[Scalar], count: u32) -> Result, Error> { + let mut shares = Vec::with_capacity(count as usize); + for i in 1..=count { + let x = index_to_scalar(i); + let scalar = evaluate_x(coeffs, x)?; + shares.push(SecretShare { + idx: i, + seckey: scalar_to_bytes(&scalar), + }); + } + Ok(shares) +} + +/// Sum a list of secret shares into a single scalar (used in DKG aggregation). +/// Mirrors `combine_shares` in the TS implementation. +pub fn combine_shares(shares: &[SecretShare]) -> [u8; 32] { + let secret = shares + .iter() + .map(|s| scalar_from_bytes(&s.seckey)) + .fold(Scalar::ZERO, |acc, cur| acc + cur); + scalar_to_bytes(&secret) +} + +/// Combine a set of shares that all have the same idx into one share. +/// Mirrors `combine_set` in the TS implementation. +pub fn combine_set(shares: &[SecretShare]) -> Result { + assert::is_equal_set(&shares.iter().map(|s| s.idx).collect::>())?; + let idx = shares[0].idx; + let seckey = combine_shares(shares); + Ok(SecretShare { idx, seckey }) +} + +/// Merge two lists of shares by combining matching indices. +/// Mirrors `merge_shares` in the TS implementation. +pub fn merge_shares( + shares_a: &[SecretShare], + shares_b: &[SecretShare], +) -> Result, Error> { + assert::equal_arr_size(shares_a, shares_b)?; + shares_a + .iter() + .map(|curr| { + let aux = shares_b + .iter() + .find(|s| s.idx == curr.idx) + .ok_or(Error::RecordNotFound(curr.idx))?; + combine_set(&[curr.clone(), aux.clone()]) + }) + .collect() +} + +/// Verify a secret share against VSS commitments. +/// Mirrors `verify_share` in the TS implementation. +pub fn verify_share( + commits: &[[u8; 33]], + share: &SecretShare, + threshold: usize, +) -> Result { + let scalar = scalar_from_bytes(&share.seckey); + let s_i = scalar_base_multi(&scalar); + + let mut s_ip = None; + for j in 0..threshold { + let point = lift_x(&commits[j])?; + let exp = pow_n(share.idx as u64, j as u64); + let prod = scalar_multi(&point, &exp); + s_ip = Some(match s_ip { + None => prod, + Some(acc) => acc + prod, + }); + } + + let s_ip = s_ip.ok_or(Error::Assertion("no commits".to_string()))?; + + // Compare x-coordinates (affine). + let s_i_affine = s_i.to_affine(); + let s_ip_affine = s_ip.to_affine(); + Ok(s_i_affine.x() == s_ip_affine.x()) +} + +/// Recover the group secret by Lagrange interpolation over a threshold of shares. +/// Mirrors `derive_shares_secret` in the TS implementation. +pub fn derive_shares_secret(shares: &[SecretShare]) -> Result<[u8; 32], Error> { + let points: Vec<(Scalar, Scalar)> = shares + .iter() + .map(|s| (index_to_scalar(s.idx), scalar_from_bytes(&s.seckey))) + .collect(); + let secret = interpolate_root(&points)?; + Ok(scalar_to_bytes(&secret)) +} diff --git a/frost-taproot/src/sign.rs b/frost-taproot/src/sign.rs new file mode 100644 index 0000000..1f4dcbb --- /dev/null +++ b/frost-taproot/src/sign.rs @@ -0,0 +1,141 @@ +// Mirrors ref/frost/src/lib/sign.ts + +use k256::{elliptic_curve::point::AffineCoordinates, Scalar}; + +use crate::commit::{get_bind_factor, get_group_binders, get_group_prefix, get_group_pubnonce}; +use crate::ecc::group::{element_add, scalar_base_multi, scalar_multi}; +use crate::ecc::util::{has_even_y, lift_x, scalar_from_bytes, scalar_to_bytes}; +use crate::helpers::get_pubkey; +use crate::poly::{index_to_scalar, interpolate_x}; +use crate::types::{ + GroupKeyContext, GroupSigningCtx, PublicNonce, SecretNonce, SecretShare, ShareSignature, +}; +use crate::Error; + +/// Produce a partial signature for a signing session. +/// Mirrors `sign_msg` in the TS implementation. +pub fn sign_msg( + ctx: &GroupSigningCtx, + share: &SecretShare, + snonce: &SecretNonce, +) -> Result { + if snonce.idx != share.idx { + return Err(Error::Assertion(format!( + "commit index does not match share index: {} !== {}", + snonce.idx, share.idx + ))); + } + + let bind_factor_bytes = get_bind_factor(&ctx.bind_factors, share.idx)?; + let bind_factor = scalar_from_bytes(&bind_factor_bytes); + + let indexes: Vec = ctx.indexes.iter().map(|&i| index_to_scalar(i)).collect(); + let coefficient = interpolate_x(&indexes, index_to_scalar(share.idx))?; + + let mut snonce_h = scalar_from_bytes(&snonce.hidden_sn); + let mut snonce_b = scalar_from_bytes(&snonce.binder_sn); + let seckey = scalar_from_bytes(&share.seckey); + + let r_elem = lift_x(&ctx.group_pn)?; + if !has_even_y(&r_elem) { + snonce_h = -snonce_h; + snonce_b = -snonce_b; + } + + // sk = parity * state * seckey (mod N) + let sk = ctx.group_pt.parity * ctx.group_pt.state * seckey; + // nk = hidden_sn + binder_sn * bind_factor + let nk = snonce_h + snonce_b * bind_factor; + // ps = challenge * coefficient * sk + nk + let ps = ctx.challenge * coefficient * sk + nk; + + Ok(ShareSignature { + idx: share.idx, + psig: scalar_to_bytes(&ps), + pubkey: get_pubkey(&share.seckey), + }) +} + +/// Aggregate partial signatures into a final BIP340 Schnorr signature (64 bytes). +/// Mirrors `combine_partial_sigs` in the TS implementation. +pub fn combine_partial_sigs( + ctx: &GroupSigningCtx, + psigs: &[ShareSignature], +) -> Result<[u8; 64], Error> { + let commit_prefix = get_group_prefix(&ctx.pnonces, &ctx.group_pk, &ctx.message); + let group_binders = get_group_binders(&ctx.pnonces, &commit_prefix); + let group_pnonce = get_group_pubnonce(&ctx.pnonces, &group_binders)?; + + // Sum all partial signatures. + let ps = psigs + .iter() + .map(|s| scalar_from_bytes(&s.psig)) + .fold(Scalar::ZERO, |acc, s| acc + s); + + // twk = challenge * parity * tweak + let twk = ctx.challenge * ctx.group_pt.parity * ctx.group_pt.tweak; + let s = ps + twk; + + // Signature = R_x (32 bytes) || s (32 bytes). + // group_pnonce is a 33-byte compressed point; x-only is bytes [1..33]. + let mut sig = [0u8; 64]; + sig[..32].copy_from_slice(&group_pnonce[1..]); + sig[32..].copy_from_slice(&scalar_to_bytes(&s)); + Ok(sig) +} + +/// Verify a partial signature from one participant. +/// Mirrors `verify_partial_sig` in the TS implementation. +pub fn verify_partial_sig( + ctx: &GroupSigningCtx, + pnonce: &PublicNonce, + share_pk: &[u8; 33], + share_psig: &[u8; 32], +) -> Result { + let binder = scalar_from_bytes(&get_bind_factor(&ctx.bind_factors, pnonce.idx)?); + + let mut hidden_elem = lift_x(&pnonce.hidden_pn)?; + let mut binder_elem = lift_x(&pnonce.binder_pn)?; + let public_elem = lift_x(share_pk)?; + + let r_elem = lift_x(&ctx.group_pn)?; + if !has_even_y(&r_elem) { + hidden_elem = -hidden_elem; + binder_elem = -binder_elem; + } + + let commit_elem = scalar_multi(&binder_elem, &binder); + let nonce_elem = element_add(Some(hidden_elem), Some(commit_elem))?; + + let indexes: Vec = ctx.indexes.iter().map(|&i| index_to_scalar(i)).collect(); + let lambda_i = interpolate_x(&indexes, index_to_scalar(pnonce.idx))?; + + let state = ctx.group_pt.parity * ctx.group_pt.state; + let chal = ctx.challenge * lambda_i * state; + + let sig = scalar_from_bytes(share_psig); + let sg = scalar_base_multi(&sig); + let pki = scalar_multi(&public_elem, &chal); + let r = element_add(Some(nonce_elem), Some(pki))?; + + // Compare x-coordinates. + Ok(sg.to_affine().x() == r.to_affine().x()) +} + +/// Verify a final aggregated BIP340 Schnorr signature. +/// Mirrors `verify_final_sig` in the TS implementation. +pub fn verify_final_sig( + ctx: &GroupKeyContext, + message: &[u8], + signature: &[u8; 64], +) -> Result { + use k256::schnorr::{Signature, VerifyingKey}; + use signature::Verifier; + + // group_pk is 33-byte compressed; BIP340 uses x-only (32 bytes). + let pk_bytes: [u8; 32] = ctx.group_pk[1..].try_into().unwrap(); + let vk = VerifyingKey::from_bytes(&pk_bytes).map_err(|_| Error::InvalidPoint)?; + let sig = Signature::try_from(signature.as_slice()).map_err(|_| Error::InvalidPoint)?; + + Ok(vk.verify(message, &sig).is_ok()) +} diff --git a/frost-taproot/src/types/commit.rs b/frost-taproot/src/types/commit.rs new file mode 100644 index 0000000..8019afa --- /dev/null +++ b/frost-taproot/src/types/commit.rs @@ -0,0 +1,53 @@ +// Mirrors ref/frost/src/types/commit.ts + +/// A participant's secret nonces for one signing round. +#[derive(Clone, Debug)] +pub struct SecretNonce { + pub idx: u32, + pub binder_sn: [u8; 32], + pub hidden_sn: [u8; 32], +} + +/// A participant's public nonce commitments for one signing round. +#[derive(Clone, Debug)] +pub struct PublicNonce { + pub idx: u32, + pub binder_pn: [u8; 33], + pub hidden_pn: [u8; 33], +} + +/// Combined secret + public nonce package for a participant. +/// Mirrors `CommitmentPackage = SecretNonce & PublicNonce`. +#[derive(Clone, Debug)] +pub struct CommitmentPackage { + pub idx: u32, + pub binder_sn: [u8; 32], + pub hidden_sn: [u8; 32], + pub binder_pn: [u8; 33], + pub hidden_pn: [u8; 33], +} + +impl CommitmentPackage { + pub fn secret_nonce(&self) -> SecretNonce { + SecretNonce { + idx: self.idx, + binder_sn: self.binder_sn, + hidden_sn: self.hidden_sn, + } + } + + pub fn public_nonce(&self) -> PublicNonce { + PublicNonce { + idx: self.idx, + binder_pn: self.binder_pn, + hidden_pn: self.hidden_pn, + } + } +} + +/// Per-participant binding factor. +#[derive(Clone, Debug)] +pub struct BindFactor { + pub idx: u32, + pub factor: [u8; 32], +} diff --git a/frost-taproot/src/types/ctx.rs b/frost-taproot/src/types/ctx.rs new file mode 100644 index 0000000..d10347f --- /dev/null +++ b/frost-taproot/src/types/ctx.rs @@ -0,0 +1,58 @@ +// Mirrors ref/frost/src/types/ctx.ts + +use k256::Scalar; + +use super::{BindFactor, PointState, PublicNonce}; + +/// Key context: the group key and its tweaked state. +#[derive(Clone, Debug)] +pub struct GroupKeyContext { + pub group_pt: PointState, + pub group_pk: [u8; 33], + pub int_pt: Option, + pub int_pk: Option<[u8; 33]>, + pub tweak: Option<[u8; 32]>, +} + +/// Commit context: everything derived from the nonces and message. +#[derive(Clone, Debug)] +pub struct GroupCommitContext { + pub bind_factors: Vec, + pub bind_prefix: Vec, + pub challenge: Scalar, + pub group_pn: [u8; 33], + pub indexes: Vec, + pub message: Vec, + pub pnonces: Vec, +} + +/// Full signing context = key context + commit context. +#[derive(Clone, Debug)] +pub struct GroupSigningCtx { + // Key context fields + pub group_pt: PointState, + pub group_pk: [u8; 33], + pub int_pt: Option, + pub int_pk: Option<[u8; 33]>, + pub tweak: Option<[u8; 32]>, + // Commit context fields + pub bind_factors: Vec, + pub bind_prefix: Vec, + pub challenge: Scalar, + pub group_pn: [u8; 33], + pub indexes: Vec, + pub message: Vec, + pub pnonces: Vec, +} + +impl GroupSigningCtx { + pub fn key_context(&self) -> GroupKeyContext { + GroupKeyContext { + group_pt: self.group_pt.clone(), + group_pk: self.group_pk, + int_pt: self.int_pt, + int_pk: self.int_pk, + tweak: self.tweak, + } + } +} diff --git a/frost-taproot/src/types/ecc.rs b/frost-taproot/src/types/ecc.rs new file mode 100644 index 0000000..7ed5a70 --- /dev/null +++ b/frost-taproot/src/types/ecc.rs @@ -0,0 +1,17 @@ +// Mirrors ref/frost/src/types/ecc.ts + +use k256::{ProjectivePoint, Scalar}; + +/// Accumulated parity/tweak state for a group key. +/// Mirrors `PointState` in the TS implementation. +#[derive(Clone, Debug)] +pub struct PointState { + /// Current parity factor (+1 or -1 as a Scalar). + pub parity: Scalar, + /// The (possibly tweaked) point. + pub point: ProjectivePoint, + /// Accumulated state (product of parities). + pub state: Scalar, + /// Accumulated tweak value. + pub tweak: Scalar, +} diff --git a/frost-taproot/src/types/mod.rs b/frost-taproot/src/types/mod.rs new file mode 100644 index 0000000..86cadb5 --- /dev/null +++ b/frost-taproot/src/types/mod.rs @@ -0,0 +1,11 @@ +mod commit; +mod ctx; +mod ecc; +mod share; +mod sign; + +pub use commit::*; +pub use ctx::*; +pub use ecc::*; +pub use share::*; +pub use sign::*; diff --git a/frost-taproot/src/types/share.rs b/frost-taproot/src/types/share.rs new file mode 100644 index 0000000..e0bf172 --- /dev/null +++ b/frost-taproot/src/types/share.rs @@ -0,0 +1,39 @@ +// Mirrors ref/frost/src/types/share.ts + +/// A participant's secret share of the group key. +#[derive(Clone, Debug)] +pub struct SecretShare { + pub idx: u32, + pub seckey: [u8; 32], +} + +/// A participant's public share (commitment). +#[derive(Clone, Debug)] +pub struct PublicShare { + pub idx: u32, + pub pubkey: [u8; 33], +} + +/// A set of secret shares with VSS commitments, produced by one dealer/participant. +#[derive(Clone, Debug)] +pub struct SecretShareSet { + pub shares: Vec, + pub vss_commits: Vec<[u8; 33]>, +} + +/// A dealer-produced share set that also includes the group public key. +#[derive(Clone, Debug)] +pub struct DealerShareSet { + pub shares: Vec, + pub vss_commits: Vec<[u8; 33]>, + pub group_pk: [u8; 33], +} + +/// A secret share set tagged with the originating participant index. +/// Mirrors `SecretSharePackage` in the TS implementation. +#[derive(Clone, Debug)] +pub struct SecretSharePackage { + pub idx: u32, + pub shares: Vec, + pub vss_commits: Vec<[u8; 33]>, +} diff --git a/frost-taproot/src/types/sign.rs b/frost-taproot/src/types/sign.rs new file mode 100644 index 0000000..81e6d72 --- /dev/null +++ b/frost-taproot/src/types/sign.rs @@ -0,0 +1,9 @@ +// Mirrors ref/frost/src/types/sign.ts + +/// A partial signature produced by one participant. +#[derive(Clone, Debug)] +pub struct ShareSignature { + pub idx: u32, + pub pubkey: [u8; 33], + pub psig: [u8; 32], +} diff --git a/frost-taproot/src/util/assert.rs b/frost-taproot/src/util/assert.rs new file mode 100644 index 0000000..bcd9bd3 --- /dev/null +++ b/frost-taproot/src/util/assert.rs @@ -0,0 +1,56 @@ +// Mirrors ref/frost/src/util/assert.ts + +use crate::Error; + +pub fn ok(value: bool, message: &str) -> Result<(), Error> { + if !value { + Err(Error::Assertion(message.to_string())) + } else { + Ok(()) + } +} + +pub fn is_included(array: &[T], item: &T) -> Result<(), Error> { + if !array.contains(item) { + Err(Error::Assertion( + "item is not included in array".to_string(), + )) + } else { + Ok(()) + } +} + +pub fn is_unique_set(array: &[T]) -> Result<(), Error> { + for x in array { + let count = array.iter().filter(|e| *e == x).count(); + if count != 1 { + return Err(Error::Assertion(format!( + "item in set is not unique: {:?}", + x + ))); + } + } + Ok(()) +} + +pub fn is_equal_set(array: &[T]) -> Result<(), Error> { + if !array.windows(2).all(|w| w[0] == w[1]) { + Err(Error::Assertion( + "set does not have equal items".to_string(), + )) + } else { + Ok(()) + } +} + +pub fn equal_arr_size(a: &[T], b: &[U]) -> Result<(), Error> { + if a.len() != b.len() { + Err(Error::Assertion(format!( + "array lengths are unequal: {} !== {}", + a.len(), + b.len() + ))) + } else { + Ok(()) + } +} diff --git a/frost-taproot/src/util/helpers.rs b/frost-taproot/src/util/helpers.rs new file mode 100644 index 0000000..de7a76e --- /dev/null +++ b/frost-taproot/src/util/helpers.rs @@ -0,0 +1,56 @@ +// Mirrors ref/frost/src/util/helpers.ts + +use rand::rngs::OsRng; +use rand::RngCore; +use sha2::{Digest, Sha256}; + +use crate::Error; + +/// Generate `size` random bytes. +pub fn random_bytes(size: usize) -> Vec { + let mut buf = vec![0u8; size]; + OsRng.fill_bytes(&mut buf); + buf +} + +/// Generate 32 random bytes. +pub fn random_bytes_32() -> [u8; 32] { + let mut buf = [0u8; 32]; + OsRng.fill_bytes(&mut buf); + buf +} + +/// Find a record by idx in a slice of items that have an `idx` field. +/// Mirrors `get_record` in the TS implementation. +pub fn get_record(records: &[T], idx: u32) -> Result { + records + .iter() + .find(|r| r.idx() == idx) + .cloned() + .ok_or_else(|| Error::RecordNotFound(idx)) +} + +/// Trait for types that carry a participant index. +pub trait HasIdx { + fn idx(&self) -> u32; +} + +/// Compute a BIP340-style tagged hash prefix: SHA256(tag) || SHA256(tag). +pub fn taghash(tag: &str) -> [u8; 64] { + let hash: [u8; 32] = Sha256::digest(tag.as_bytes()).into(); + let mut out = [0u8; 64]; + out[..32].copy_from_slice(&hash); + out[32..].copy_from_slice(&hash); + out +} + +/// BIP340 tagged hash: SHA256(SHA256(tag) || SHA256(tag) || data...). +pub fn hash340(tag: &str, data: &[&[u8]]) -> [u8; 32] { + let prefix = taghash(tag); + let mut hasher = Sha256::new(); + hasher.update(&prefix); + for d in data { + hasher.update(d); + } + hasher.finalize().into() +} diff --git a/frost-taproot/src/util/mod.rs b/frost-taproot/src/util/mod.rs new file mode 100644 index 0000000..b32f3f0 --- /dev/null +++ b/frost-taproot/src/util/mod.rs @@ -0,0 +1,4 @@ +pub mod assert; +pub mod helpers; + +pub use helpers::*; diff --git a/frost-taproot/src/vss.rs b/frost-taproot/src/vss.rs new file mode 100644 index 0000000..181d4aa --- /dev/null +++ b/frost-taproot/src/vss.rs @@ -0,0 +1,55 @@ +// Mirrors ref/frost/src/lib/vss.ts +// Verifiable Secret Sharing: coefficient generation and commitments. + +use k256::Scalar; + +use crate::ecc::group::{scalar_base_multi, serialize_element}; +use crate::ecc::util::mod_n; +use crate::util::assert; +use crate::util::helpers::random_bytes_32; +use crate::Error; +use k256::U256; + +/// Create polynomial coefficients for a Shamir secret sharing scheme. +/// If fewer secrets are provided than `threshold`, the remaining coefficients are random. +/// Mirrors `create_share_coeffs` in the TS implementation. +pub fn create_share_coeffs(secrets: &[[u8; 32]], threshold: usize) -> Vec { + let mut coeffs = Vec::with_capacity(threshold); + for i in 0..threshold { + let coeff = if let Some(s) = secrets.get(i) { + mod_n(U256::from_be_slice(s)) + } else { + mod_n(U256::from_be_slice(&random_bytes_32())) + }; + coeffs.push(coeff); + } + coeffs +} + +/// Compute VSS commitments: one compressed public key per coefficient. +/// Mirrors `get_share_commits` in the TS implementation. +pub fn get_share_commits(coeffs: &[Scalar]) -> Vec<[u8; 33]> { + coeffs + .iter() + .map(|c| serialize_element(&scalar_base_multi(c))) + .collect() +} + +/// Merge two sets of VSS commitments by adding corresponding points. +/// Mirrors `merge_share_commits` in the TS implementation. +pub fn merge_share_commits( + commits_a: &[[u8; 33]], + commits_b: &[[u8; 33]], +) -> Result, Error> { + assert::equal_arr_size(commits_a, commits_b)?; + commits_a + .iter() + .zip(commits_b.iter()) + .map(|(a, b)| { + let pa = crate::ecc::util::lift_x(a)?; + let pb = crate::ecc::util::lift_x(b)?; + let pc = pa + pb; + Ok(serialize_element(&pc)) + }) + .collect() +}