diff --git a/.github/workflows/no-std.yml b/.github/workflows/no-std.yml index 94446f04..3791eb38 100644 --- a/.github/workflows/no-std.yml +++ b/.github/workflows/no-std.yml @@ -30,5 +30,8 @@ jobs: with: github-token: ${{ inputs.github-token }} + - name: Install RISC-V Toolchain + run: sudo apt install -y gcc-riscv64-unknown-elf gcc-multilib + - name: Verify no-std builds - run: cd tests/no-std && cargo build --target riscv32imac-unknown-none-elf + run: cd tests/no-std && CFLAGS=-I/usr/include cargo build --target riscv32imac-unknown-none-elf diff --git a/Cargo.lock b/Cargo.lock index 5dd26751..f28d4ac4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -462,6 +462,7 @@ dependencies = [ "bech32 0.9.1", "bitcoin-private", "bitcoin_hashes", + "core2 0.3.3", "hex_lit", "secp256k1", "serde", @@ -481,7 +482,6 @@ dependencies = [ "flexible-transcript", "hex", "k256", - "lazy_static", "modular-frost", "rand_core 0.6.4", "reqwest", @@ -489,6 +489,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.7", + "std-shims", "thiserror", "tokio", "zeroize", @@ -501,6 +502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" dependencies = [ "bitcoin-private", + "core2 0.3.3", "serde", ] @@ -884,7 +886,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd94671561e36e4e7de75f753f577edafb0e7c05d6e4547229fdf7938fbcd2c3" dependencies = [ - "core2", + "core2 0.4.0", "multibase", "multihash 0.18.1", "serde", @@ -1140,6 +1142,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" +dependencies = [ + "memchr", +] + [[package]] name = "core2" version = "0.4.0" @@ -4800,7 +4811,7 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "blake3", - "core2", + "core2 0.4.0", "digest 0.10.7", "multihash-derive 0.8.0", "sha2 0.10.7", @@ -4814,7 +4825,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd59dcc2bbe70baabeac52cd22ae52c55eefe6c38ff11a9439f16a350a939f2" dependencies = [ - "core2", + "core2 0.4.0", "unsigned-varint", ] @@ -4827,7 +4838,7 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "blake3", - "core2", + "core2 0.4.0", "digest 0.10.7", "multihash-derive 0.9.0", "ripemd", @@ -4857,7 +4868,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "890e72cb7396cb99ed98c1246a97b243cc16394470d94e0bc8b0c2c11d84290e" dependencies = [ - "core2", + "core2 0.4.0", "multihash 0.19.0", "multihash-derive-impl", ] @@ -8100,6 +8111,7 @@ dependencies = [ name = "serai-no-std-tests" version = "0.1.0" dependencies = [ + "bitcoin-serai", "ciphersuite", "dalek-ff-group", "dkg", diff --git a/coins/bitcoin/Cargo.toml b/coins/bitcoin/Cargo.toml index 95fdbbe7..9fee4ae4 100644 --- a/coins/bitcoin/Cargo.toml +++ b/coins/bitcoin/Cargo.toml @@ -8,25 +8,27 @@ authors = ["Luke Parker ", "Vrx "] edition = "2021" [dependencies] -lazy_static = "1" -thiserror = "1" +std-shims = { version = "0.1.1", path = "../../common/std-shims", default-features = false } -zeroize = "^1.5" -rand_core = "0.6" +thiserror = { version = "1", optional = true } -sha2 = "0.10" +zeroize = { version = "^1.5", default-features = false } +rand_core = { version = "0.6", default-features = false } -secp256k1 = { version = "0.27", features = ["global-context"] } -bitcoin = { version = "0.30", features = ["serde"] } +sha2 = { version = "0.10", default-features = false } -k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic", "bits"] } -transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", features = ["recommended"] } -frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", features = ["secp256k1"] } +secp256k1 = { version = "0.27", default-features = false } +bitcoin = { version = "0.30", default-features = false, features = ["no-std"] } -hex = "0.4" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -reqwest = { version = "0.11", features = ["json"] } +k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] } + +transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", features = ["recommended"], optional = true } +frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", features = ["secp256k1"], optional = true } + +hex = { version = "0.4", optional = true } +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } +reqwest = { version = "0.11", features = ["json"], optional = true } [dev-dependencies] frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] } @@ -34,4 +36,29 @@ frost = { package = "modular-frost", path = "../../crypto/frost", features = ["t tokio = { version = "1", features = ["full"] } [features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + "rand_core/std", + + "sha2/std", + + "secp256k1/std", + "bitcoin/std", + "bitcoin/serde", + + "k256/std", + + "transcript", + "frost", + + "hex", + "serde", + "serde_json", + "reqwest" +] hazmat = [] +default = ["std"] diff --git a/coins/bitcoin/src/crypto.rs b/coins/bitcoin/src/crypto.rs index 741a0289..9cf0c8ca 100644 --- a/coins/bitcoin/src/crypto.rs +++ b/coins/bitcoin/src/crypto.rs @@ -1,26 +1,6 @@ -use core::fmt::Debug; -use std::io; - -use lazy_static::lazy_static; - -use zeroize::Zeroizing; -use rand_core::{RngCore, CryptoRng}; - -use sha2::{Digest, Sha256}; -use transcript::Transcript; - -use secp256k1::schnorr::Signature; use k256::{ - elliptic_curve::{ - ops::Reduce, - sec1::{Tag, ToEncodedPoint}, - }, - U256, Scalar, ProjectivePoint, -}; -use frost::{ - curve::{Ciphersuite, Secp256k1}, - Participant, ThresholdKeys, ThresholdView, FrostError, - algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr}, + elliptic_curve::sec1::{Tag, ToEncodedPoint}, + ProjectivePoint, }; use bitcoin::key::XOnlyPublicKey; @@ -40,6 +20,7 @@ pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey { /// Make a point even by adding the generator until it is even. /// /// Returns the even point and the amount of additions required. +#[cfg(any(feature = "std", feature = "hazmat"))] pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) { let mut c = 0; while key.to_encoded_point(true).tag() == Tag::CompressedOddY { @@ -49,116 +30,143 @@ pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) { (key, c) } -/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm. -/// -/// If passed an odd nonce, it will have the generator added until it is even. -/// -/// If the key is odd, this will panic. -#[derive(Clone, Copy, Debug)] -pub struct Hram; +#[cfg(feature = "std")] +mod frost_crypto { + use core::fmt::Debug; + use std_shims::{sync::OnceLock, vec::Vec, io}; -lazy_static! { - static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into(); -} + use zeroize::Zeroizing; + use rand_core::{RngCore, CryptoRng}; -#[allow(non_snake_case)] -impl HramTrait for Hram { - fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { - // Convert the nonce to be even - let (R, _) = make_even(*R); + use sha2::{Digest, Sha256}; + use transcript::Transcript; - let mut data = Sha256::new(); - data.update(*TAG_HASH); - data.update(*TAG_HASH); - data.update(x(&R)); - data.update(x(A)); - data.update(m); + use secp256k1::schnorr::Signature; + use k256::{elliptic_curve::ops::Reduce, U256, Scalar}; - Scalar::reduce(U256::from_be_slice(&data.finalize())) - } -} - -/// BIP-340 Schnorr signature algorithm. -/// -/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic. -#[derive(Clone)] -pub struct Schnorr(FrostSchnorr); -impl Schnorr { - /// Construct a Schnorr algorithm continuing the specified transcript. - pub fn new(transcript: T) -> Schnorr { - Schnorr(FrostSchnorr::new(transcript)) - } -} - -impl Algorithm for Schnorr { - type Transcript = T; - type Addendum = (); - type Signature = Signature; - - fn transcript(&mut self) -> &mut Self::Transcript { - self.0.transcript() - } - - fn nonces(&self) -> Vec> { - self.0.nonces() - } - - fn preprocess_addendum( - &mut self, - rng: &mut R, - keys: &ThresholdKeys, - ) { - self.0.preprocess_addendum(rng, keys) - } - - fn read_addendum(&self, reader: &mut R) -> io::Result { - self.0.read_addendum(reader) - } - - fn process_addendum( - &mut self, - view: &ThresholdView, - i: Participant, - addendum: (), - ) -> Result<(), FrostError> { - self.0.process_addendum(view, i, addendum) - } - - fn sign_share( - &mut self, - params: &ThresholdView, - nonce_sums: &[Vec<::G>], - nonces: Vec::F>>, - msg: &[u8], - ) -> ::F { - self.0.sign_share(params, nonce_sums, nonces, msg) - } - - #[must_use] - fn verify( - &self, - group_key: ProjectivePoint, - nonces: &[Vec], - sum: Scalar, - ) -> Option { - self.0.verify(group_key, nonces, sum).map(|mut sig| { - // Make the R of the final signature even - let offset; - (sig.R, offset) = make_even(sig.R); - // s = r + cx. Since we added to the r, add to s - sig.s += Scalar::from(offset); - // Convert to a secp256k1 signature - Signature::from_slice(&sig.serialize()[1 ..]) - .expect("couldn't convert SchnorrSignature to Signature") - }) - } - - fn verify_share( - &self, - verification_share: ProjectivePoint, - nonces: &[Vec], - share: Scalar, - ) -> Result, ()> { - self.0.verify_share(verification_share, nonces, share) + use frost::{ + curve::{Ciphersuite, Secp256k1}, + Participant, ThresholdKeys, ThresholdView, FrostError, + algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr}, + }; + + use super::*; + + /// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm. + /// + /// If passed an odd nonce, it will have the generator added until it is even. + /// + /// If the key is odd, this will panic. + #[derive(Clone, Copy, Debug)] + pub struct Hram; + + static TAG_HASH_CELL: OnceLock<[u8; 32]> = OnceLock::new(); + #[allow(non_snake_case)] + fn TAG_HASH() -> [u8; 32] { + *TAG_HASH_CELL.get_or_init(|| Sha256::digest(b"BIP0340/challenge").into()) + } + + #[allow(non_snake_case)] + impl HramTrait for Hram { + fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar { + // Convert the nonce to be even + let (R, _) = make_even(*R); + + let mut data = Sha256::new(); + data.update(TAG_HASH()); + data.update(TAG_HASH()); + data.update(x(&R)); + data.update(x(A)); + data.update(m); + + Scalar::reduce(U256::from_be_slice(&data.finalize())) + } + } + + /// BIP-340 Schnorr signature algorithm. + /// + /// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic. + #[derive(Clone)] + pub struct Schnorr(FrostSchnorr); + impl Schnorr { + /// Construct a Schnorr algorithm continuing the specified transcript. + pub fn new(transcript: T) -> Schnorr { + Schnorr(FrostSchnorr::new(transcript)) + } + } + + impl Algorithm for Schnorr { + type Transcript = T; + type Addendum = (); + type Signature = Signature; + + fn transcript(&mut self) -> &mut Self::Transcript { + self.0.transcript() + } + + fn nonces(&self) -> Vec> { + self.0.nonces() + } + + fn preprocess_addendum( + &mut self, + rng: &mut R, + keys: &ThresholdKeys, + ) { + self.0.preprocess_addendum(rng, keys) + } + + fn read_addendum(&self, reader: &mut R) -> io::Result { + self.0.read_addendum(reader) + } + + fn process_addendum( + &mut self, + view: &ThresholdView, + i: Participant, + addendum: (), + ) -> Result<(), FrostError> { + self.0.process_addendum(view, i, addendum) + } + + fn sign_share( + &mut self, + params: &ThresholdView, + nonce_sums: &[Vec<::G>], + nonces: Vec::F>>, + msg: &[u8], + ) -> ::F { + self.0.sign_share(params, nonce_sums, nonces, msg) + } + + #[must_use] + fn verify( + &self, + group_key: ProjectivePoint, + nonces: &[Vec], + sum: Scalar, + ) -> Option { + self.0.verify(group_key, nonces, sum).map(|mut sig| { + // Make the R of the final signature even + let offset; + (sig.R, offset) = make_even(sig.R); + // s = r + cx. Since we added to the r, add to s + sig.s += Scalar::from(offset); + // Convert to a secp256k1 signature + Signature::from_slice(&sig.serialize()[1 ..]) + .expect("couldn't convert SchnorrSignature to Signature") + }) + } + + fn verify_share( + &self, + verification_share: ProjectivePoint, + nonces: &[Vec], + share: Scalar, + ) -> Result, ()> { + self.0.verify_share(verification_share, nonces, share) + } } } +#[cfg(feature = "std")] +pub use frost_crypto::*; diff --git a/coins/bitcoin/src/lib.rs b/coins/bitcoin/src/lib.rs index fbb52f60..0aa17709 100644 --- a/coins/bitcoin/src/lib.rs +++ b/coins/bitcoin/src/lib.rs @@ -1,5 +1,9 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(not(feature = "std"))] +extern crate alloc; /// The bitcoin Rust library. pub use bitcoin; @@ -13,6 +17,7 @@ pub(crate) mod crypto; /// Wallet functionality to create transactions. pub mod wallet; /// A minimal asynchronous Bitcoin RPC client. +#[cfg(feature = "std")] pub mod rpc; #[cfg(test)] diff --git a/coins/bitcoin/src/tests/crypto.rs b/coins/bitcoin/src/tests/crypto.rs index b40f3677..f827591d 100644 --- a/coins/bitcoin/src/tests/crypto.rs +++ b/coins/bitcoin/src/tests/crypto.rs @@ -2,7 +2,7 @@ use rand_core::OsRng; use sha2::{Digest, Sha256}; -use secp256k1::{SECP256K1, Message}; +use secp256k1::{Secp256k1 as BContext, Message}; use k256::Scalar; use transcript::{Transcript, RecommendedTranscript}; @@ -37,7 +37,7 @@ fn test_algorithm() { &Sha256::digest(MESSAGE), ); - SECP256K1 + BContext::new() .verify_schnorr( &sig, &Message::from(Hash::hash(MESSAGE)), diff --git a/coins/bitcoin/src/wallet/mod.rs b/coins/bitcoin/src/wallet/mod.rs index d1146dba..fc875faf 100644 --- a/coins/bitcoin/src/wallet/mod.rs +++ b/coins/bitcoin/src/wallet/mod.rs @@ -1,33 +1,45 @@ -use std::{ - io::{self, Read, Write}, +use std_shims::{ + vec::Vec, collections::HashMap, + io::{self, Write}, }; +#[cfg(feature = "std")] +use std_shims::io::Read; use k256::{ elliptic_curve::sec1::{Tag, ToEncodedPoint}, Scalar, ProjectivePoint, }; + +#[cfg(feature = "std")] use frost::{ curve::{Ciphersuite, Secp256k1}, ThresholdKeys, }; use bitcoin::{ - consensus::encode::{Decodable, serialize}, + consensus::encode::serialize, key::TweakedPublicKey, address::Payload, OutPoint, ScriptBuf, TxOut, Transaction, Block, }; +#[cfg(feature = "std")] +use bitcoin::consensus::encode::Decodable; -use crate::crypto::{x_only, make_even}; +use crate::crypto::x_only; +#[cfg(feature = "std")] +use crate::crypto::make_even; +#[cfg(feature = "std")] mod send; +#[cfg(feature = "std")] pub use send::*; /// Tweak keys to ensure they're usable with Bitcoin. /// /// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're /// even. +#[cfg(feature = "std")] pub fn tweak_keys(keys: &ThresholdKeys) -> ThresholdKeys { let (_, offset) = make_even(keys.group_key()); keys.offset(Scalar::from(offset)) @@ -72,6 +84,7 @@ impl ReceivedOutput { } /// Read a ReceivedOutput from a generic satisfying Read. + #[cfg(feature = "std")] pub fn read(r: &mut R) -> io::Result { Ok(ReceivedOutput { offset: Secp256k1::read_F(r)?, @@ -89,9 +102,9 @@ impl ReceivedOutput { w.write_all(&serialize(&self.outpoint)) } - /// Serialize a ReceivedOutput to a Vec. + /// Serialize a ReceivedOutput to a `Vec`. pub fn serialize(&self) -> Vec { - let mut res = vec![]; + let mut res = Vec::new(); self.write(&mut res).unwrap(); res } @@ -143,7 +156,7 @@ impl Scanner { /// Scan a transaction. pub fn scan_transaction(&self, tx: &Transaction) -> Vec { - let mut res = vec![]; + let mut res = Vec::new(); for (vout, output) in tx.output.iter().enumerate() { // If the vout index exceeds 2**32, stop scanning outputs let Ok(vout) = u32::try_from(vout) else { break }; @@ -165,7 +178,7 @@ impl Scanner { /// must be immediately spendable, a post-processing pass is needed to remove those outputs. /// Alternatively, scan_transaction can be called on `block.txdata[1 ..]`. pub fn scan_block(&self, block: &Block) -> Vec { - let mut res = vec![]; + let mut res = Vec::new(); for tx in &block.txdata { res.extend(self.scan_transaction(tx)); } diff --git a/coins/bitcoin/src/wallet/send.rs b/coins/bitcoin/src/wallet/send.rs index 43fa54bf..a7e4abc8 100644 --- a/coins/bitcoin/src/wallet/send.rs +++ b/coins/bitcoin/src/wallet/send.rs @@ -1,4 +1,4 @@ -use std::{ +use std_shims::{ io::{self, Read}, collections::HashMap, }; diff --git a/coins/bitcoin/tests/runner.rs b/coins/bitcoin/tests/runner.rs index 73f6bc5e..6491d538 100644 --- a/coins/bitcoin/tests/runner.rs +++ b/coins/bitcoin/tests/runner.rs @@ -1,9 +1,13 @@ +use std::sync::OnceLock; + use bitcoin_serai::rpc::Rpc; use tokio::sync::Mutex; -lazy_static::lazy_static! { - pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(()); +static SEQUENTIAL_CELL: OnceLock> = OnceLock::new(); +#[allow(non_snake_case)] +pub fn SEQUENTIAL() -> &'static Mutex<()> { + SEQUENTIAL_CELL.get_or_init(|| Mutex::new(())) } #[allow(dead_code)] @@ -30,7 +34,7 @@ macro_rules! async_sequential { $( #[tokio::test] async fn $name() { - let guard = runner::SEQUENTIAL.lock().await; + let guard = runner::SEQUENTIAL().lock().await; let local = tokio::task::LocalSet::new(); local.run_until(async move { if let Err(err) = tokio::task::spawn_local(async move { $body }).await { diff --git a/tests/no-std/Cargo.toml b/tests/no-std/Cargo.toml index 65a16053..a086092e 100644 --- a/tests/no-std/Cargo.toml +++ b/tests/no-std/Cargo.toml @@ -30,5 +30,7 @@ dkg = { path = "../../crypto/dkg", default-features = false } # modular-frost = { path = "../../crypto/frost", default-features = false } # frost-schnorrkel = { path = "../../crypto/schnorrkel", default-features = false } +bitcoin-serai = { path = "../../coins/bitcoin", default-features = false, features = ["hazmat"] } + monero-generators = { path = "../../coins/monero/generators", default-features = false } monero-serai = { path = "../../coins/monero", default-features = false }