diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..f4e8c00 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "wasm32-unknown-unknown" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 01eb0d6..cb78705 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -71,7 +71,11 @@ jobs: tar zxvf binaryen-version_100-x86_64-linux.tar.gz binaryen-version_100/bin/wasm-opt - name: Build wasm - run: export PATH=$PATH:./binaryen-version_100/bin/ && make build-wasm + run: | + export PATH=$PATH:./binaryen-version_100/bin/ + make build-wasm + shasum -a 256 lib/secp256k1.wasm + rustc --version --verbose - name: Build JS run: make build-js @@ -123,18 +127,19 @@ jobs: path: lib - name: Create package - run: npm pack + run: npm pack && shasum -a 256 tiny-secp256k1-*.tgz - name: Upload package uses: actions/upload-artifact@v2 with: name: package - path: tiny-secp256k1-* + path: tiny-secp256k1-*.tgz benchmark: name: Benchmark needs: [test] continue-on-error: true + if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest steps: - name: Fetch code diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dcd2ab5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "rust-analyzer.check.targets": [ + "wasm32-unknown-unknown", + ], + "rust-analyzer.check.command": "clippy" +} diff --git a/Cargo.lock b/Cargo.lock index a465944..12b09e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,129 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + [[package]] name = "cc" -version = "1.0.71" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +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 = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "secp256k1" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25996b82292a7a57ed3508f052cfff8640d38d32018784acd714758b43da9c8f" +dependencies = [ + "secp256k1-sys", +] [[package]] name = "secp256k1-sys" @@ -18,8 +136,94 @@ dependencies = [ ] [[package]] -name = "secp256k1-wasm" -version = "0.0.0" +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ - "secp256k1-sys", + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tiny-secp256k1" +version = "0.2.0" +dependencies = [ + "getrandom", + "rand", + "secp256k1", +] + +[[package]] +name = "tiny-secp256k1-wasm" +version = "0.1.0" +dependencies = [ + "tiny-secp256k1", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", ] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" diff --git a/Cargo.toml b/Cargo.toml index 7b8d691..d6b2869 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,24 +1,12 @@ -[package] -name = "secp256k1-wasm" -version = "0.0.0" -authors = ["Kirill Fomichev "] -edition = "2021" -description = "A Rust library for building tiny-secp256k1 WASM." -license = "MIT" -publish = false - -[lib] -crate-type = ["cdylib"] - -[dependencies.secp256k1-sys] -version = "=0.8.1" -default-features = false -features=[ - "recovery", - "lowmemory" +[workspace] +resolver = "2" +members = [ + "tiny-secp256k1", + "tiny-secp256k1-wasm", ] [profile.release] +opt-level = "z" lto = true panic = "abort" codegen-units = 1 diff --git a/Makefile b/Makefile index b538f5f..6b6f04b 100644 --- a/Makefile +++ b/Makefile @@ -18,14 +18,14 @@ build-js: .PHONY: build-wasm build-wasm: - RUSTFLAGS="-C link-args=-zstack-size=655360" cargo build --target wasm32-unknown-unknown --release - mkdir -p lib && cp -f target/wasm32-unknown-unknown/release/secp256k1_wasm.wasm lib/secp256k1.wasm + RUSTFLAGS="-C link-args=-zstack-size=655360" cargo build --target wasm32-unknown-unknown -p tiny-secp256k1-wasm --release + mkdir -p lib && cp -f target/wasm32-unknown-unknown/release/tiny_secp256k1_wasm.wasm lib/secp256k1.wasm wasm-opt -O4 --strip-debug --strip-producers --output lib/secp256k1.wasm lib/secp256k1.wasm .PHONY: build-wasm-debug build-wasm-debug: - RUSTFLAGS="-C link-args=-zstack-size=655360" cargo build --target wasm32-unknown-unknown - mkdir -p lib && cp -f target/wasm32-unknown-unknown/debug/secp256k1_wasm.wasm lib/secp256k1.wasm + RUSTFLAGS="-C link-args=-zstack-size=655360" cargo build --target wasm32-unknown-unknown -p tiny-secp256k1-wasm + mkdir -p lib && cp -f target/wasm32-unknown-unknown/debug/tiny_secp256k1_wasm.wasm lib/secp256k1.wasm .PHONY: clean clean: clean-deps clean-built @@ -68,7 +68,8 @@ install-js-deps: .PHONY: lint lint: cargo fmt -- --check - cargo clippy --target wasm32-unknown-unknown + cargo clippy -p tiny-secp256k1-wasm --target wasm32-unknown-unknown --no-default-features + cargo clippy -p tiny-secp256k1 --target x86_64-unknown-linux-gnu --no-default-features npx eslint $(eslint_files) .PHONY: test diff --git a/benches/index.js b/benches/index.js index bd586af..a35fe5e 100644 --- a/benches/index.js +++ b/benches/index.js @@ -12,6 +12,8 @@ import { fschnorrTweak, } from "./fixtures.js"; +const WASM_ONLY = !!process.env.WASM_ONLY; + // import { loadAddon as _loadAddon } from "../lib/addon.js"; // function loadAddon(location) { // const path = new URL(location, import.meta.url).pathname; @@ -27,26 +29,30 @@ const modules = [ name: "tiny-secp256k1 (WASM)", secp256k1: tiny_secp256k1, }, - { - name: "tiny-secp256k1@1.1.6 (C++ addon, NAN/V8)", - secp256k1: tiny_secp256k1_prev_native, - }, - { - name: "tiny-secp256k1@1.1.6 (elliptic)", - secp256k1: tiny_secp256k1_prev_js, - }, - { - name: "secp256k1@4.0.2 (C++ addon, N-API)", - secp256k1: cryptocoinjs_secp256k1.native, - }, - { - name: "secp256k1@4.0.2 (elliptic)", - secp256k1: cryptocoinjs_secp256k1.js, - }, - { - name: "noble-secp256k1@1.1.2 (BigInt)", - secp256k1: noble_secp256k1, - }, + ...(WASM_ONLY + ? [] + : [ + { + name: "tiny-secp256k1@1.1.6 (C++ addon, NAN/V8)", + secp256k1: tiny_secp256k1_prev_native, + }, + { + name: "tiny-secp256k1@1.1.6 (elliptic)", + secp256k1: tiny_secp256k1_prev_js, + }, + { + name: "secp256k1@4.0.2 (C++ addon, N-API)", + secp256k1: cryptocoinjs_secp256k1.native, + }, + { + name: "secp256k1@4.0.2 (elliptic)", + secp256k1: cryptocoinjs_secp256k1.js, + }, + { + name: "noble-secp256k1@1.1.2 (BigInt)", + secp256k1: noble_secp256k1, + }, + ]), ]; const benchmarks = [ diff --git a/examples/README.md b/examples/README.md index 500482b..06a8000 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,3 +27,14 @@ npm start ``` and open [http://localhost:8080/](http://localhost:8080/). + +## simple-timing-test + +Run this command multiple times to confirm there is no difference in processing time with EC point multiplication. + +Variance is usually under 0.5%, but it fluctuates. For comparison, Native JS of v1 of this library has a 40% variance +between private key 2 and n - 1. + +``` +node ./run.js +``` \ No newline at end of file diff --git a/examples/simple-timing-test/run.js b/examples/simple-timing-test/run.js new file mode 100644 index 0000000..d0cb403 --- /dev/null +++ b/examples/simple-timing-test/run.js @@ -0,0 +1,70 @@ +import { fromHex } from "uint8array-tools"; +import * as secp256k1 from "../../lib/index.js"; + +const TWO = fromHex("0".repeat(63) + "2"); +const TOP_BIT = fromHex("8" + "0".repeat(63)); +const N_LESS_ONE = fromHex( + "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140" +); + +const ITER = 10000; + +const BENCHES = [ + { + name: "Privkey two", + func() { + secp256k1.pointFromScalar(TWO); + }, + }, + { + name: "Highest bit", + func() { + secp256k1.pointFromScalar(TOP_BIT); + }, + }, + { + name: "Max privkey", + func() { + secp256k1.pointFromScalar(N_LESS_ONE); + }, + }, +]; + +function warmup() { + for (let i = 0; i < 4000; i++) { + BENCHES[0].func(); + } +} + +function bench(name, f, iter) { + const start = process.hrtime.bigint(); + for (let i = 0; i < iter; i++) { + f(); + } + const end = process.hrtime.bigint(); + const duration = end - start; + console.log(`${name}: ${(duration / BigInt(iter)).toString(10)} ns per op`); + return duration; +} + +async function main() { + warmup(); + const durations = BENCHES.map((b) => bench(b.name, b.func, ITER)).map( + (v) => v / BigInt(1e6) + ); + durations.sort(); + const [hi, low] = [ + Number(durations[durations.length - 1]), + Number(durations[0]), + ]; + const hiLowDiff = hi - low; + const avg = (hi + low) / 2; + const pct = (hi / avg - 1) * 100; + console.log(`High Low Diff: ${hiLowDiff} ms diff for ${ITER} iterations`); + console.log(`±${pct.toFixed(2)}% variance`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/rust-toolchain b/rust-toolchain index 9006c0b..dc87e8a 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.70.0 \ No newline at end of file +1.74.0 diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 63382f9..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,588 +0,0 @@ -#![deny(clippy::all)] -#![deny(clippy::pedantic)] -#![no_std] - -#[cfg(not(target_arch = "wasm32"))] -compile_error!("Only `wasm32` target_arch is supported."); - -#[panic_handler] -#[cfg(target_arch = "wasm32")] -fn panic(_info: &core::panic::PanicInfo) -> ! { - core::arch::wasm32::unreachable() -} - -use core::ptr::NonNull; - -use secp256k1_sys::{ - secp256k1_context_no_precomp, secp256k1_context_preallocated_create, - secp256k1_context_preallocated_size, secp256k1_context_randomize, secp256k1_ec_pubkey_combine, - secp256k1_ec_pubkey_create, secp256k1_ec_pubkey_parse, secp256k1_ec_pubkey_serialize, - secp256k1_ec_pubkey_tweak_add, secp256k1_ec_pubkey_tweak_mul, secp256k1_ec_seckey_negate, - secp256k1_ec_seckey_tweak_add, secp256k1_ecdsa_sign, secp256k1_ecdsa_signature_normalize, - secp256k1_ecdsa_signature_parse_compact, secp256k1_ecdsa_signature_serialize_compact, - secp256k1_ecdsa_verify, secp256k1_keypair_create, secp256k1_keypair_xonly_pub, - secp256k1_nonce_function_rfc6979, secp256k1_schnorrsig_sign, secp256k1_schnorrsig_verify, - secp256k1_xonly_pubkey_from_pubkey, secp256k1_xonly_pubkey_parse, - secp256k1_xonly_pubkey_serialize, secp256k1_xonly_pubkey_tweak_add, - secp256k1_xonly_pubkey_tweak_add_check, types::c_void, Context, KeyPair, PublicKey, Signature, - XOnlyPublicKey, SECP256K1_SER_COMPRESSED, SECP256K1_SER_UNCOMPRESSED, SECP256K1_START_SIGN, - SECP256K1_START_VERIFY, -}; - -use secp256k1_sys::recovery::{ - secp256k1_ecdsa_recover, secp256k1_ecdsa_recoverable_signature_parse_compact, - secp256k1_ecdsa_recoverable_signature_serialize_compact, secp256k1_ecdsa_sign_recoverable, - RecoverableSignature, -}; - -#[link(wasm_import_module = "./validate_error.js")] -extern "C" { - #[link_name = "throwError"] - fn throw_error(errcode: usize); -} - -#[link(wasm_import_module = "./rand.js")] -extern "C" { - #[link_name = "generateInt32"] - fn generate_int32() -> i32; -} - -type InvalidInputResult = Result; - -#[allow(clippy::large_stack_arrays)] -static CONTEXT_BUFFER: [u8; 192] = [0; 192]; -static mut CONTEXT_SEED: [u8; 32] = [0; 32]; - -const PRIVATE_KEY_SIZE: usize = 32; -const PUBLIC_KEY_COMPRESSED_SIZE: usize = 33; -const PUBLIC_KEY_UNCOMPRESSED_SIZE: usize = 65; -const X_ONLY_PUBLIC_KEY_SIZE: usize = 32; -const TWEAK_SIZE: usize = 32; -const HASH_SIZE: usize = 32; -const EXTRA_DATA_SIZE: usize = 32; -const SIGNATURE_SIZE: usize = 64; - -const ERROR_BAD_PRIVATE: usize = 0; -const ERROR_BAD_POINT: usize = 1; -// const ERROR_BAD_TWEAK: usize = 2; -// const ERROR_BAD_HASH: usize = 3; -const ERROR_BAD_SIGNATURE: usize = 4; -// const ERROR_BAD_EXTRA_DATA: usize = 5; -// const ERROR_BAD_PARITY: usize = 6; - -#[no_mangle] -pub static mut PRIVATE_INPUT: [u8; PRIVATE_KEY_SIZE] = [0; PRIVATE_KEY_SIZE]; -#[no_mangle] -pub static mut PUBLIC_KEY_INPUT: [u8; PUBLIC_KEY_UNCOMPRESSED_SIZE] = - [0; PUBLIC_KEY_UNCOMPRESSED_SIZE]; -#[no_mangle] -pub static PUBLIC_KEY_INPUT2: [u8; PUBLIC_KEY_UNCOMPRESSED_SIZE] = - [0; PUBLIC_KEY_UNCOMPRESSED_SIZE]; -#[no_mangle] -pub static mut X_ONLY_PUBLIC_KEY_INPUT: [u8; X_ONLY_PUBLIC_KEY_SIZE] = [0; X_ONLY_PUBLIC_KEY_SIZE]; -#[no_mangle] -pub static mut X_ONLY_PUBLIC_KEY_INPUT2: [u8; X_ONLY_PUBLIC_KEY_SIZE] = [0; X_ONLY_PUBLIC_KEY_SIZE]; -#[no_mangle] -pub static mut TWEAK_INPUT: [u8; TWEAK_SIZE] = [0; TWEAK_SIZE]; -#[no_mangle] -pub static HASH_INPUT: [u8; HASH_SIZE] = [0; HASH_SIZE]; -#[no_mangle] -pub static EXTRA_DATA_INPUT: [u8; EXTRA_DATA_SIZE] = [0; EXTRA_DATA_SIZE]; -#[no_mangle] -pub static mut SIGNATURE_INPUT: [u8; SIGNATURE_SIZE] = [0; SIGNATURE_SIZE]; - -macro_rules! jstry { - ($value:expr) => { - jstry!($value, ()) - }; - ($value:expr, $ret:expr) => { - match $value { - Ok(value) => value, - Err(code) => { - throw_error(code); - return $ret; - } - } - }; -} - -fn initialize_context_seed() { - unsafe { - for offset in (0..8).map(|v| v * 4) { - let value = generate_int32(); - let bytes: [u8; 4] = value.to_ne_bytes(); - CONTEXT_SEED[offset..offset + 4].copy_from_slice(&bytes); - } - } -} - -fn get_context() -> *const Context { - static mut CONTEXT: *const Context = core::ptr::null(); - unsafe { - if CONTEXT_SEED[0] == 0 { - let size = - secp256k1_context_preallocated_size(SECP256K1_START_SIGN | SECP256K1_START_VERIFY); - assert_eq!(size, CONTEXT_BUFFER.len()); - let ctx = secp256k1_context_preallocated_create( - NonNull::new(CONTEXT_BUFFER.as_ptr() as *mut c_void).expect("Not null"), - SECP256K1_START_SIGN | SECP256K1_START_VERIFY, - ); - initialize_context_seed(); - let retcode = secp256k1_context_randomize(ctx, CONTEXT_SEED.as_ptr()); - CONTEXT_SEED[0] = 1; - CONTEXT_SEED[1..].fill(0); - assert_eq!(retcode, 1); - CONTEXT = ctx.as_ptr(); - } - CONTEXT - } -} - -unsafe fn create_keypair(input: *const u8) -> InvalidInputResult { - let mut kp = KeyPair::new(); - if secp256k1_keypair_create(get_context(), &mut kp, input) == 1 { - Ok(kp) - } else { - Err(ERROR_BAD_PRIVATE) - } -} - -unsafe fn x_only_pubkey_from_pubkey(input: *const u8, inputlen: usize) -> (XOnlyPublicKey, i32) { - let mut xonly_pk = XOnlyPublicKey::new(); - let mut parity: i32 = 0; - let pubkey = jstry!(pubkey_parse(input, inputlen), (xonly_pk, parity)); - x_only_pubkey_from_pubkey_struct(&mut xonly_pk, &mut parity, &pubkey) -} - -unsafe fn x_only_pubkey_from_pubkey_struct( - xonly_pk: &mut XOnlyPublicKey, - parity: &mut i32, - pubkey: &PublicKey, -) -> (XOnlyPublicKey, i32) { - assert_eq!( - secp256k1_xonly_pubkey_from_pubkey(get_context(), xonly_pk, parity, pubkey), - 1 - ); - (*xonly_pk, *parity) -} - -unsafe fn pubkey_parse(input: *const u8, inputlen: usize) -> InvalidInputResult { - let mut pk = PublicKey::new(); - if secp256k1_ec_pubkey_parse(secp256k1_context_no_precomp, &mut pk, input, inputlen) == 1 { - Ok(pk) - } else { - Err(ERROR_BAD_POINT) - } -} - -unsafe fn x_only_pubkey_parse(input: *const u8) -> InvalidInputResult { - let mut pk = XOnlyPublicKey::new(); - if secp256k1_xonly_pubkey_parse(secp256k1_context_no_precomp, &mut pk, input) == 1 { - Ok(pk) - } else { - Err(ERROR_BAD_POINT) - } -} - -unsafe fn pubkey_serialize(pk: &PublicKey, output: *mut u8, mut outputlen: usize) { - let flags = if outputlen == PUBLIC_KEY_COMPRESSED_SIZE { - SECP256K1_SER_COMPRESSED - } else { - SECP256K1_SER_UNCOMPRESSED - }; - assert_eq!( - secp256k1_ec_pubkey_serialize( - secp256k1_context_no_precomp, - output, - &mut outputlen, - pk, - flags, - ), - 1 - ); -} - -unsafe fn x_only_pubkey_serialize(pk: &XOnlyPublicKey, output: *mut u8) { - assert_eq!( - secp256k1_xonly_pubkey_serialize(secp256k1_context_no_precomp, output, pk), - 1 - ); -} - -#[no_mangle] -#[export_name = "initializeContext"] -pub extern "C" fn initialize_context() { - get_context(); -} - -#[no_mangle] -#[export_name = "isPoint"] -pub extern "C" fn is_point(inputlen: usize) -> usize { - unsafe { - if inputlen == X_ONLY_PUBLIC_KEY_SIZE { - x_only_pubkey_parse(PUBLIC_KEY_INPUT.as_ptr()).map_or_else(|_error| 0, |_pk| 1) - } else { - pubkey_parse(PUBLIC_KEY_INPUT.as_ptr(), inputlen).map_or_else(|_error| 0, |_pk| 1) - } - } -} - -// We know (ptrs.len() as i32) will not trunc or wrap since it is always 2. -#[allow(clippy::cast_possible_truncation)] -#[allow(clippy::cast_possible_wrap)] -#[no_mangle] -#[export_name = "pointAdd"] -pub extern "C" fn point_add(inputlen: usize, inputlen2: usize, outputlen: usize) -> i32 { - unsafe { - let pk1 = jstry!(pubkey_parse(PUBLIC_KEY_INPUT.as_ptr(), inputlen), 0); - let pk2 = jstry!(pubkey_parse(PUBLIC_KEY_INPUT2.as_ptr(), inputlen2), 0); - let mut pk = PublicKey::new(); - let ptrs = [&pk1, &pk2]; - if secp256k1_ec_pubkey_combine( - secp256k1_context_no_precomp, - &mut pk, - ptrs.as_ptr().cast::<*const PublicKey>(), - ptrs.len() as i32, - ) == 1 - { - pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen); - 1 - } else { - 0 - } - } -} - -#[no_mangle] -#[export_name = "pointAddScalar"] -pub extern "C" fn point_add_scalar(inputlen: usize, outputlen: usize) -> i32 { - unsafe { - let mut pk = jstry!(pubkey_parse(PUBLIC_KEY_INPUT.as_ptr(), inputlen), 0); - if secp256k1_ec_pubkey_tweak_add(get_context(), &mut pk, TWEAK_INPUT.as_ptr()) == 1 { - pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen); - 1 - } else { - 0 - } - } -} - -#[no_mangle] -#[export_name = "xOnlyPointAddTweak"] -pub extern "C" fn x_only_point_add_tweak() -> i32 { - unsafe { - let mut xonly_pk = jstry!(x_only_pubkey_parse(X_ONLY_PUBLIC_KEY_INPUT.as_ptr()), 0); - let mut pubkey = PublicKey::new(); - if secp256k1_xonly_pubkey_tweak_add( - get_context(), - &mut pubkey, - &xonly_pk, - TWEAK_INPUT.as_ptr(), - ) != 1 - { - // infinity point - return -1; - } - let mut parity: i32 = 0; - x_only_pubkey_from_pubkey_struct(&mut xonly_pk, &mut parity, &pubkey); - x_only_pubkey_serialize(&xonly_pk, X_ONLY_PUBLIC_KEY_INPUT.as_mut_ptr()); - parity - } -} - -#[no_mangle] -#[export_name = "xOnlyPointAddTweakCheck"] -pub extern "C" fn x_only_point_add_tweak_check(tweaked_parity: i32) -> i32 { - unsafe { - let xonly_pk = jstry!(x_only_pubkey_parse(X_ONLY_PUBLIC_KEY_INPUT.as_ptr()), 0); - let tweaked_key_ptr = X_ONLY_PUBLIC_KEY_INPUT2.as_ptr(); - jstry!(x_only_pubkey_parse(tweaked_key_ptr), 0); - - secp256k1_xonly_pubkey_tweak_add_check( - get_context(), - tweaked_key_ptr, - tweaked_parity, - &xonly_pk, - TWEAK_INPUT.as_ptr(), - ) - } -} - -#[no_mangle] -#[export_name = "pointCompress"] -pub extern "C" fn point_compress(inputlen: usize, outputlen: usize) { - unsafe { - let pk = jstry!(pubkey_parse(PUBLIC_KEY_INPUT.as_ptr(), inputlen)); - pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen); - } -} - -#[no_mangle] -#[export_name = "pointFromScalar"] -pub extern "C" fn point_from_scalar(outputlen: usize) -> i32 { - unsafe { - let mut pk = PublicKey::new(); - if secp256k1_ec_pubkey_create(get_context(), &mut pk, PRIVATE_INPUT.as_ptr()) == 1 { - pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen); - 1 - } else { - 0 - } - } -} - -#[allow(clippy::missing_panics_doc)] -#[no_mangle] -#[export_name = "xOnlyPointFromScalar"] -pub extern "C" fn x_only_point_from_scalar() -> i32 { - unsafe { - let keypair = jstry!(create_keypair(PRIVATE_INPUT.as_ptr()), 0); - let mut xonly_pk = XOnlyPublicKey::new(); - let mut parity: i32 = 0; // TODO: Should we return this somehow? - assert_eq!( - secp256k1_keypair_xonly_pub(get_context(), &mut xonly_pk, &mut parity, &keypair), - 1 - ); - x_only_pubkey_serialize(&xonly_pk, X_ONLY_PUBLIC_KEY_INPUT.as_mut_ptr()); - 1 - } -} - -#[no_mangle] -#[export_name = "xOnlyPointFromPoint"] -pub extern "C" fn x_only_point_from_point(inputlen: usize) -> i32 { - unsafe { - let (xonly_pk, _parity) = x_only_pubkey_from_pubkey(PUBLIC_KEY_INPUT.as_ptr(), inputlen); - x_only_pubkey_serialize(&xonly_pk, X_ONLY_PUBLIC_KEY_INPUT.as_mut_ptr()); - 1 - } -} - -#[no_mangle] -#[export_name = "pointMultiply"] -pub extern "C" fn point_multiply(inputlen: usize, outputlen: usize) -> i32 { - unsafe { - let mut pk = jstry!(pubkey_parse(PUBLIC_KEY_INPUT.as_ptr(), inputlen), 0); - if secp256k1_ec_pubkey_tweak_mul(get_context(), &mut pk, TWEAK_INPUT.as_ptr()) == 1 { - pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen); - 1 - } else { - 0 - } - } -} - -#[no_mangle] -#[export_name = "privateAdd"] -pub extern "C" fn private_add() -> i32 { - unsafe { - i32::from( - secp256k1_ec_seckey_tweak_add( - secp256k1_context_no_precomp, - PRIVATE_INPUT.as_mut_ptr(), - TWEAK_INPUT.as_ptr(), - ) == 1, - ) - } -} - -#[allow(clippy::missing_panics_doc)] -#[no_mangle] -#[export_name = "privateSub"] -pub extern "C" fn private_sub() -> i32 { - unsafe { - assert_eq!( - secp256k1_ec_seckey_negate(secp256k1_context_no_precomp, TWEAK_INPUT.as_mut_ptr()), - 1 - ); - i32::from( - secp256k1_ec_seckey_tweak_add( - secp256k1_context_no_precomp, - PRIVATE_INPUT.as_mut_ptr(), - TWEAK_INPUT.as_ptr(), - ) == 1, - ) - } -} - -#[allow(clippy::missing_panics_doc)] -#[no_mangle] -#[export_name = "privateNegate"] -pub extern "C" fn private_negate() { - unsafe { - assert_eq!( - secp256k1_ec_seckey_negate(secp256k1_context_no_precomp, PRIVATE_INPUT.as_mut_ptr()), - 1 - ); - } -} - -#[allow(clippy::missing_panics_doc)] -#[no_mangle] -pub extern "C" fn sign(extra_data: i32) { - unsafe { - let mut sig = Signature::new(); - let noncedata = if extra_data == 0 { - core::ptr::null() - } else { - EXTRA_DATA_INPUT.as_ptr() - }; - - assert_eq!( - secp256k1_ecdsa_sign( - get_context(), - &mut sig, - HASH_INPUT.as_ptr(), - PRIVATE_INPUT.as_ptr(), - secp256k1_nonce_function_rfc6979, - noncedata.cast() - ), - 1 - ); - - assert_eq!( - secp256k1_ecdsa_signature_serialize_compact( - secp256k1_context_no_precomp, - SIGNATURE_INPUT.as_mut_ptr(), - &sig, - ), - 1 - ); - } -} - -#[allow(clippy::missing_panics_doc)] -#[no_mangle] -#[export_name = "signRecoverable"] -pub extern "C" fn sign_recoverable(extra_data: i32) -> i32 { - unsafe { - let mut sig = RecoverableSignature::new(); - let noncedata = if extra_data == 0 { - core::ptr::null() - } else { - EXTRA_DATA_INPUT.as_ptr() - }; - - assert_eq!( - secp256k1_ecdsa_sign_recoverable( - get_context(), - &mut sig, - HASH_INPUT.as_ptr(), - PRIVATE_INPUT.as_ptr(), - secp256k1_nonce_function_rfc6979, - noncedata.cast() - ), - 1 - ); - - let mut recid: i32 = 0; - secp256k1_ecdsa_recoverable_signature_serialize_compact( - secp256k1_context_no_precomp, - SIGNATURE_INPUT.as_mut_ptr(), - &mut recid, - &sig, - ); - recid - } -} - -#[allow(clippy::missing_panics_doc)] -#[no_mangle] -#[export_name = "signSchnorr"] -pub extern "C" fn sign_schnorr(extra_data: i32) { - unsafe { - let mut keypair = KeyPair::new(); - let noncedata = if extra_data == 0 { - core::ptr::null() - } else { - EXTRA_DATA_INPUT.as_ptr() - } - .cast::(); - - assert_eq!( - secp256k1_keypair_create(get_context(), &mut keypair, PRIVATE_INPUT.as_ptr()), - 1 - ); - - assert_eq!( - secp256k1_schnorrsig_sign( - get_context(), - SIGNATURE_INPUT.as_mut_ptr(), - HASH_INPUT.as_ptr(), - &keypair, - noncedata.cast() - ), - 1 - ); - } -} - -#[no_mangle] -pub extern "C" fn verify(inputlen: usize, strict: i32) -> i32 { - unsafe { - let pk = jstry!(pubkey_parse(PUBLIC_KEY_INPUT.as_ptr(), inputlen), 0); - - let mut signature = Signature::new(); - if secp256k1_ecdsa_signature_parse_compact( - secp256k1_context_no_precomp, - &mut signature, - SIGNATURE_INPUT.as_ptr(), - ) == 0 - { - throw_error(ERROR_BAD_SIGNATURE); - return 0; - } - - if strict == 0 { - secp256k1_ecdsa_signature_normalize( - secp256k1_context_no_precomp, - &mut signature, - &signature, - ); - } - - i32::from(secp256k1_ecdsa_verify(get_context(), &signature, HASH_INPUT.as_ptr(), &pk) == 1) - } -} - -#[no_mangle] -pub extern "C" fn recover(outputlen: usize, recid: i32) -> i32 { - unsafe { - let mut signature = RecoverableSignature::new(); - if secp256k1_ecdsa_recoverable_signature_parse_compact( - secp256k1_context_no_precomp, - &mut signature, - SIGNATURE_INPUT.as_ptr(), - recid, - ) == 0 - { - throw_error(ERROR_BAD_SIGNATURE); - return 0; - } - - let mut pk = PublicKey::new(); - if secp256k1_ecdsa_recover(get_context(), &mut pk, &signature, HASH_INPUT.as_ptr()) == 1 { - pubkey_serialize(&pk, PUBLIC_KEY_INPUT.as_mut_ptr(), outputlen); - 1 - } else { - 0 - } - } -} - -#[no_mangle] -#[export_name = "verifySchnorr"] -pub extern "C" fn verify_schnorr() -> i32 { - unsafe { - let pk = jstry!(x_only_pubkey_parse(X_ONLY_PUBLIC_KEY_INPUT.as_ptr()), 0); - i32::from( - secp256k1_schnorrsig_verify( - get_context(), - SIGNATURE_INPUT.as_ptr(), - HASH_INPUT.as_ptr(), - 32, - &pk, - ) == 1, - ) - } -} diff --git a/src_ts/validate_error.ts b/src_ts/validate_error.ts index 61c3734..3411834 100644 --- a/src_ts/validate_error.ts +++ b/src_ts/validate_error.ts @@ -20,6 +20,6 @@ const ERRORS_MESSAGES = { export function throwError(errcode: number): never { const message = - ERRORS_MESSAGES[errcode.toString()] || `Unknow error code: ${errcode}`; + ERRORS_MESSAGES[errcode.toString()] || `Unknown error code: ${errcode}`; throw new TypeError(message); } diff --git a/src_ts/wasm_loader.browser.ts b/src_ts/wasm_loader.browser.ts index bf36a0e..36dbdc5 100644 --- a/src_ts/wasm_loader.browser.ts +++ b/src_ts/wasm_loader.browser.ts @@ -2,4 +2,5 @@ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import * as wasm from "./secp256k1.wasm"; +wasm.initializeContext(); export default wasm; diff --git a/src_ts/wasm_loader.ts b/src_ts/wasm_loader.ts index 8706d82..1946de1 100644 --- a/src_ts/wasm_loader.ts +++ b/src_ts/wasm_loader.ts @@ -57,4 +57,6 @@ interface Secp256k1WASM { recover: (outputlen: number, recoveryId: RecoveryIdType) => number; } -export default instance.exports as unknown as Secp256k1WASM; +const wasm = instance.exports as unknown as Secp256k1WASM; +wasm.initializeContext(); +export default wasm; diff --git a/tests/ecdsa.js b/tests/ecdsa.js index d5ce1bb..368d6b2 100644 --- a/tests/ecdsa.js +++ b/tests/ecdsa.js @@ -37,8 +37,8 @@ export default function (secp256k1) { const expected = fromHex(f.signature); t.same( - secp256k1.sign(m, d), - expected, + toHex(secp256k1.sign(m, d)), + toHex(expected), `sign(${f.m}, ...) == ${f.signature}` ); } @@ -116,8 +116,8 @@ export default function (secp256k1) { const res = secp256k1.signRecoverable(m, d); t.same( - res.signature, - expected, + toHex(res.signature), + toHex(expected), `signRecoverable(${f.m}, ...) == { signature: "${f.signature}", ...}` ); diff --git a/tiny-secp256k1-wasm/Cargo.toml b/tiny-secp256k1-wasm/Cargo.toml new file mode 100644 index 0000000..5d01ea4 --- /dev/null +++ b/tiny-secp256k1-wasm/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tiny-secp256k1-wasm" +version = "0.1.0" +authors = [ + "Kirill Fomichev ", + "Jonathan Underwood " +] +edition = "2021" +description = "A Rust library for building tiny-secp256k1 WASM." +license = "MIT" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +tiny-secp256k1 = { path = "../tiny-secp256k1", default-features = false, features = [ "minimal_validation" ] } diff --git a/tiny-secp256k1-wasm/src/into_pubkey.rs b/tiny-secp256k1-wasm/src/into_pubkey.rs new file mode 100644 index 0000000..024e8e8 --- /dev/null +++ b/tiny-secp256k1-wasm/src/into_pubkey.rs @@ -0,0 +1,67 @@ +use super::throw_error; +// const ZERO32: [u8; 32] = [0_u8; 32]; +pub(crate) struct GeneralKey<'a>(pub(crate) &'a [u8; 65], pub(crate) &'a usize); + +impl<'a> From> for tiny_secp256k1::Pubkey { + fn from(v: GeneralKey<'a>) -> tiny_secp256k1::Pubkey { + match v.1 { + 65 => { + // if v.0[0] != 4_u8 { + // bad_point!(); + // } + tiny_secp256k1::Pubkey::Uncompressed(*v.0) + } + 33 => { + // if (v.0[0] != 2_u8 && v.0[0] != 3_u8) || v.0[33..65] != ZERO32 { + // bad_point!(); + // } + tiny_secp256k1::Pubkey::Compressed(unsafe { + *v.0.as_ptr().cast::<[u8; 33]>().as_ref().unwrap() + }) + } + 32 => { + // if v.0[32] != 0_u8 || v.0[33..65] != ZERO32 { + // bad_point!(); + // } + tiny_secp256k1::Pubkey::XOnly(unsafe { + *v.0.as_ptr().cast::<[u8; 32]>().as_ref().unwrap() + }) + } + _ => { + bad_point!(); + } + } + } +} + +impl<'a> From> for tiny_secp256k1::PubkeyRef<'a> { + fn from(v: GeneralKey<'a>) -> tiny_secp256k1::PubkeyRef<'a> { + match v.1 { + 65 => { + // if v.0[0] != 4_u8 { + // bad_point!(); + // } + tiny_secp256k1::PubkeyRef::Uncompressed(v.0) + } + 33 => { + // if (v.0[0] != 2_u8 && v.0[0] != 3_u8) || v.0[33..65] != ZERO32 { + // bad_point!(); + // } + tiny_secp256k1::PubkeyRef::Compressed(unsafe { + v.0.as_ptr().cast::<[u8; 33]>().as_ref().unwrap() + }) + } + 32 => { + // if v.0[32] != 0_u8 || v.0[33..65] != ZERO32 { + // bad_point!(); + // } + tiny_secp256k1::PubkeyRef::XOnly(unsafe { + v.0.as_ptr().cast::<[u8; 32]>().as_ref().unwrap() + }) + } + _ => { + bad_point!(); + } + } + } +} diff --git a/tiny-secp256k1-wasm/src/lib.rs b/tiny-secp256k1-wasm/src/lib.rs new file mode 100644 index 0000000..06312ba --- /dev/null +++ b/tiny-secp256k1-wasm/src/lib.rs @@ -0,0 +1,444 @@ +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![no_std] + +#[cfg(not(target_arch = "wasm32"))] +compile_error!("Only `wasm32` target_arch is supported."); + +#[panic_handler] +#[cfg(target_arch = "wasm32")] +fn panic(_info: &core::panic::PanicInfo) -> ! { + core::arch::wasm32::unreachable() +} + +#[macro_use] +mod macros; +mod into_pubkey; +use core::cell::UnsafeCell; +use into_pubkey::GeneralKey; + +#[link(wasm_import_module = "./validate_error.js")] +extern "C" { + #[link_name = "throwError"] + fn throw_error(errcode: usize); +} + +#[link(wasm_import_module = "./rand.js")] +extern "C" { + #[link_name = "generateInt32"] + fn generate_int32() -> i32; +} + +#[repr(transparent)] +pub struct UCWrapper(pub(crate) UnsafeCell); +unsafe impl Sync for UCWrapper {} +impl UCWrapper { + unsafe fn get_ref(&self) -> &T { + self.0.get().as_ref().unwrap() + } + #[allow(clippy::mut_from_ref)] + unsafe fn get_mut(&self) -> &mut T { + self.0.get().as_mut().unwrap() + } + // If needed in the future + // unsafe fn get_copy(&self) -> T { + // *self.0.get().as_mut().unwrap() + // } +} + +const PRIVATE_KEY_SIZE: usize = 32; +const PUBLIC_KEY_UNCOMPRESSED_SIZE: usize = 65; +const X_ONLY_PUBLIC_KEY_SIZE: usize = 32; +const TWEAK_SIZE: usize = 32; +const HASH_SIZE: usize = 32; +const EXTRA_DATA_SIZE: usize = 32; +const SIGNATURE_SIZE: usize = 64; + +#[no_mangle] +pub static PRIVATE_INPUT: UCWrapper<[u8; PRIVATE_KEY_SIZE]> = + UCWrapper(UnsafeCell::new([0; PRIVATE_KEY_SIZE])); +#[no_mangle] +pub static PUBLIC_KEY_INPUT: UCWrapper<[u8; PUBLIC_KEY_UNCOMPRESSED_SIZE]> = + UCWrapper(UnsafeCell::new([0; PUBLIC_KEY_UNCOMPRESSED_SIZE])); +#[no_mangle] +pub static PUBLIC_KEY_INPUT2: UCWrapper<[u8; PUBLIC_KEY_UNCOMPRESSED_SIZE]> = + UCWrapper(UnsafeCell::new([0; PUBLIC_KEY_UNCOMPRESSED_SIZE])); +#[no_mangle] +pub static X_ONLY_PUBLIC_KEY_INPUT: UCWrapper<[u8; X_ONLY_PUBLIC_KEY_SIZE]> = + UCWrapper(UnsafeCell::new([0; X_ONLY_PUBLIC_KEY_SIZE])); +#[no_mangle] +pub static X_ONLY_PUBLIC_KEY_INPUT2: UCWrapper<[u8; X_ONLY_PUBLIC_KEY_SIZE]> = + UCWrapper(UnsafeCell::new([0; X_ONLY_PUBLIC_KEY_SIZE])); +#[no_mangle] +pub static TWEAK_INPUT: UCWrapper<[u8; TWEAK_SIZE]> = UCWrapper(UnsafeCell::new([0; TWEAK_SIZE])); +#[no_mangle] +pub static HASH_INPUT: UCWrapper<[u8; HASH_SIZE]> = UCWrapper(UnsafeCell::new([0; HASH_SIZE])); +#[no_mangle] +pub static EXTRA_DATA_INPUT: UCWrapper<[u8; EXTRA_DATA_SIZE]> = + UCWrapper(UnsafeCell::new([0; EXTRA_DATA_SIZE])); +#[no_mangle] +pub static SIGNATURE_INPUT: UCWrapper<[u8; SIGNATURE_SIZE]> = + UCWrapper(UnsafeCell::new([0; SIGNATURE_SIZE])); + +fn build_context() { + let mut seed = [0_u8; 32]; + for offset in (0..8).map(|v| v * 4) { + let value = unsafe { generate_int32() }; + let bytes: [u8; 4] = value.to_ne_bytes(); + seed[offset..offset + 4].copy_from_slice(&bytes); + } + tiny_secp256k1::set_context(&seed); +} + +#[no_mangle] +#[export_name = "initializeContext"] +pub extern "C" fn initialize_context() { + build_context(); +} + +#[no_mangle] +#[export_name = "isPoint"] +pub extern "C" fn is_point(inputlen: usize) -> usize { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let pubkey_input = unsafe { PUBLIC_KEY_INPUT.get_ref() }; + let pubkey = GeneralKey(pubkey_input, &inputlen); + usize::from(tiny_secp256k1::is_point(&pubkey.into())) +} + +#[no_mangle] +#[export_name = "pointAdd"] +pub extern "C" fn point_add(inputlen: usize, inputlen2: usize, compressed: usize) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (pubkey_input, pubkey_input2) = + unsafe { (PUBLIC_KEY_INPUT.get_mut(), PUBLIC_KEY_INPUT2.get_ref()) }; + let pubkey = jstry_opt!( + tiny_secp256k1::point_add( + &GeneralKey(pubkey_input, &inputlen).into(), + &GeneralKey(pubkey_input2, &inputlen2).into(), + pubkey_size_to_opt_bool!(compressed) + ), + 0 + ); + let size = pubkey.len(); + pubkey_input[..size].copy_from_slice(&pubkey.as_slice()[..size]); + 1 +} + +#[no_mangle] +#[export_name = "pointAddScalar"] +pub extern "C" fn point_add_scalar(inputlen: usize, compressed: usize) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (pubkey_input, tweak_input) = + unsafe { (PUBLIC_KEY_INPUT.get_mut(), TWEAK_INPUT.get_ref()) }; + let pubkey = jstry_opt!( + tiny_secp256k1::point_add_scalar( + &GeneralKey(pubkey_input, &inputlen).into(), + tweak_input, + pubkey_size_to_opt_bool!(compressed) + ), + 0 + ); + let size = pubkey.len(); + pubkey_input[..size].copy_from_slice(&pubkey.as_slice()[..size]); + 1 +} + +#[no_mangle] +#[export_name = "xOnlyPointAddTweak"] +pub extern "C" fn x_only_point_add_tweak() -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (x_only_pubkey_input, tweak_input) = + unsafe { (X_ONLY_PUBLIC_KEY_INPUT.get_mut(), TWEAK_INPUT.get_ref()) }; + let (x_only_point, parity) = jstry_opt!( + tiny_secp256k1::x_only_point_add_tweak(x_only_pubkey_input, tweak_input), + -1 + ); + x_only_pubkey_input[..X_ONLY_PUBLIC_KEY_SIZE] + .copy_from_slice(&x_only_point[..X_ONLY_PUBLIC_KEY_SIZE]); + parity +} + +#[no_mangle] +#[export_name = "xOnlyPointAddTweakCheck"] +pub extern "C" fn x_only_point_add_tweak_check(tweaked_parity: i32) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (x_only_pubkey_input, x_only_pubkey_input2, tweak_input) = unsafe { + ( + X_ONLY_PUBLIC_KEY_INPUT.get_ref(), + X_ONLY_PUBLIC_KEY_INPUT2.get_ref(), + TWEAK_INPUT.get_ref(), + ) + }; + let tweaked_parity = parity_to_opt_int!(tweaked_parity); + i32::from(jstry!( + tiny_secp256k1::x_only_point_add_tweak_check( + x_only_pubkey_input, + &(*x_only_pubkey_input2, tweaked_parity), + tweak_input + ), + 0 + )) +} + +#[no_mangle] +#[export_name = "pointCompress"] +pub extern "C" fn point_compress(inputlen: usize, compressed: usize) { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let pubkey_input = unsafe { PUBLIC_KEY_INPUT.get_mut() }; + let pubkey = jstry!(tiny_secp256k1::point_compress( + &GeneralKey(pubkey_input, &inputlen).into(), + pubkey_size_to_opt_bool!(compressed) + )); + let size = pubkey.len(); + pubkey_input[..size].copy_from_slice(&pubkey.as_slice()[..size]); +} + +#[no_mangle] +#[export_name = "pointFromScalar"] +pub extern "C" fn point_from_scalar(compressed: usize) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (public_key_input, private_input) = + unsafe { (PUBLIC_KEY_INPUT.get_mut(), PRIVATE_INPUT.get_ref()) }; + let pubkey = jstry_opt!( + tiny_secp256k1::point_from_scalar(private_input, pubkey_size_to_opt_bool!(compressed)), + 0 + ); + let size = pubkey.len(); + public_key_input[..size].copy_from_slice(&pubkey.as_slice()[..size]); + 1 +} + +#[no_mangle] +#[export_name = "xOnlyPointFromScalar"] +#[allow(clippy::missing_panics_doc)] +pub extern "C" fn x_only_point_from_scalar() { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (x_only_public_key_input, private_input) = + unsafe { (X_ONLY_PUBLIC_KEY_INPUT.get_mut(), PRIVATE_INPUT.get_ref()) }; + let (point, _parity) = + tiny_secp256k1::x_only_point_from_scalar(private_input).expect("JS side validation"); + x_only_public_key_input.copy_from_slice(&point); +} + +#[no_mangle] +#[export_name = "xOnlyPointFromPoint"] +pub extern "C" fn x_only_point_from_point(inputlen: usize) { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (x_only_public_key_input, public_key_input) = unsafe { + ( + X_ONLY_PUBLIC_KEY_INPUT.get_mut(), + PUBLIC_KEY_INPUT.get_ref(), + ) + }; + let (point, _parity) = jstry!(tiny_secp256k1::x_only_point_from_point( + &GeneralKey(public_key_input, &inputlen).into() + )); + x_only_public_key_input.copy_from_slice(&point); +} + +#[no_mangle] +#[export_name = "pointMultiply"] +pub extern "C" fn point_multiply(inputlen: usize, compressed: usize) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (public_key_input, tweak_input) = + unsafe { (PUBLIC_KEY_INPUT.get_mut(), TWEAK_INPUT.get_ref()) }; + let pubkey = jstry_opt!( + tiny_secp256k1::point_multiply( + &GeneralKey(public_key_input, &inputlen).into(), + tweak_input, + pubkey_size_to_opt_bool!(compressed) + ), + 0 + ); + let size = pubkey.len(); + public_key_input[..size].copy_from_slice(&pubkey.as_slice()[..size]); + 1 +} + +#[no_mangle] +#[export_name = "privateAdd"] +pub extern "C" fn private_add() -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (private_input, tweak_input) = unsafe { (PRIVATE_INPUT.get_mut(), TWEAK_INPUT.get_ref()) }; + let private = jstry!(tiny_secp256k1::private_add(private_input, tweak_input), 0); + private_input.copy_from_slice(&priv_or_ret!(private, 0)); + 1 +} + +#[no_mangle] +#[export_name = "privateSub"] +pub extern "C" fn private_sub() -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (private_input, tweak_input) = unsafe { (PRIVATE_INPUT.get_mut(), TWEAK_INPUT.get_ref()) }; + let private = jstry!(tiny_secp256k1::private_sub(private_input, tweak_input), 0); + private_input.copy_from_slice(&priv_or_ret!(private, 0)); + 1 +} + +#[no_mangle] +#[export_name = "privateNegate"] +pub extern "C" fn private_negate() { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let private_input = unsafe { PRIVATE_INPUT.get_mut() }; + let private = jstry!(tiny_secp256k1::private_negate(private_input)); + private_input.copy_from_slice(&private); +} + +#[no_mangle] +pub extern "C" fn sign(extra_data: i32) { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (signature_input, hash_input, private_input, extra_data_input) = unsafe { + ( + SIGNATURE_INPUT.get_mut(), + HASH_INPUT.get_ref(), + PRIVATE_INPUT.get_ref(), + EXTRA_DATA_INPUT.get_ref(), + ) + }; + signature_input.copy_from_slice(&jstry!(tiny_secp256k1::sign( + hash_input, + private_input, + if extra_data == 0 { + None + } else { + Some(extra_data_input) + }, + ))); +} + +#[no_mangle] +#[export_name = "signRecoverable"] +pub extern "C" fn sign_recoverable(extra_data: i32) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (signature_input, hash_input, private_input, extra_data_input) = unsafe { + ( + SIGNATURE_INPUT.get_mut(), + HASH_INPUT.get_ref(), + PRIVATE_INPUT.get_ref(), + EXTRA_DATA_INPUT.get_ref(), + ) + }; + let sig = jstry!( + tiny_secp256k1::sign_recoverable( + hash_input, + private_input, + if extra_data == 0 { + None + } else { + Some(extra_data_input) + }, + ), + 0 + ); + signature_input.copy_from_slice(&sig.1); + sig.0.to_i32() +} + +#[no_mangle] +#[export_name = "signSchnorr"] +pub extern "C" fn sign_schnorr(extra_data: i32) { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (signature_input, hash_input, private_input, extra_data_input) = unsafe { + ( + SIGNATURE_INPUT.get_mut(), + HASH_INPUT.get_ref(), + PRIVATE_INPUT.get_ref(), + EXTRA_DATA_INPUT.get_ref(), + ) + }; + signature_input.copy_from_slice(&jstry!(tiny_secp256k1::sign_schnorr( + hash_input, + private_input, + if extra_data == 0 { + None + } else { + Some(extra_data_input) + }, + ))); +} + +#[no_mangle] +pub extern "C" fn verify(inputlen: usize, strict: i32) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (signature_input, hash_input, public_key_input) = unsafe { + ( + SIGNATURE_INPUT.get_ref(), + HASH_INPUT.get_ref(), + PUBLIC_KEY_INPUT.get_ref(), + ) + }; + i32::from(jstry!( + tiny_secp256k1::verify( + hash_input, + &GeneralKey(public_key_input, &inputlen).into(), + signature_input, + match strict { + 1 => Some(true), + 0 => Some(false), + _ => None, + } + ), + 0 + )) +} + +#[allow(clippy::missing_panics_doc)] +#[no_mangle] +pub extern "C" fn recover(outputlen: usize, recid: i32) -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (public_key_input, signature_input, hash_input) = unsafe { + ( + PUBLIC_KEY_INPUT.get_mut(), + SIGNATURE_INPUT.get_ref(), + HASH_INPUT.get_ref(), + ) + }; + let pubkey = jstry!( + tiny_secp256k1::recover( + hash_input, + signature_input, + tiny_secp256k1::RecoveryId::from_i32(recid).unwrap(), + pubkey_size_to_opt_bool!(outputlen), + ), + 0 + ); + let size = pubkey.len(); + public_key_input[..size].copy_from_slice(&pubkey.as_slice()[..size]); + 1 +} + +#[no_mangle] +#[export_name = "verifySchnorr"] +pub extern "C" fn verify_schnorr() -> i32 { + // Safety: WASM is single threaded and we only get references once per function. + // Also, the same static memory area is not called twice. + let (signature_input, hash_input, x_only_public_key_input) = unsafe { + ( + SIGNATURE_INPUT.get_ref(), + HASH_INPUT.get_ref(), + X_ONLY_PUBLIC_KEY_INPUT.get_ref(), + ) + }; + i32::from(jstry!( + tiny_secp256k1::verify_schnorr(hash_input, x_only_public_key_input, signature_input), + 0 + )) +} diff --git a/tiny-secp256k1-wasm/src/macros.rs b/tiny-secp256k1-wasm/src/macros.rs new file mode 100644 index 0000000..70115c1 --- /dev/null +++ b/tiny-secp256k1-wasm/src/macros.rs @@ -0,0 +1,72 @@ +macro_rules! jstry { + ($value:expr) => { + jstry!($value, ()) + }; + ($value:expr, $ret:expr) => { + match $value { + Ok(value) => value, + Err(code) => { + unsafe { throw_error(code as usize) }; + return $ret; + } + } + }; +} + +macro_rules! jstry_opt { + ($value:expr, $ret:expr) => { + match $value { + Ok(value) => { + if let Some(v) = value { + v + } else { + return $ret; + } + } + Err(code) => { + unsafe { throw_error(code as usize) }; + return $ret; + } + } + }; +} + +macro_rules! pubkey_size_to_opt_bool { + ($value:expr) => { + match $value { + 65 => Some(false), + 33 => Some(true), + _ => None, + } + }; +} + +macro_rules! parity_to_opt_int { + ($value:expr) => { + match $value { + 0 => Some(0), + 1 => Some(1), + _ => None, + } + }; +} + +macro_rules! priv_or_ret { + ($private:expr, $ret:expr) => { + match $private { + Some(prv) => prv, + None => { + return $ret; + } + } + }; +} + +macro_rules! bad_point { + () => { + unsafe { + throw_error(tiny_secp256k1::Error::BadPoint as usize); + } + panic!("Bad Point"); + }; +} diff --git a/tiny-secp256k1/Cargo.toml b/tiny-secp256k1/Cargo.toml new file mode 100644 index 0000000..0833d07 --- /dev/null +++ b/tiny-secp256k1/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tiny-secp256k1" +version = "0.2.0" +authors = [ + "Kirill Fomichev ", + "Jonathan Underwood " +] +edition = "2021" +description = "A Rust library for building tiny-secp256k1 WASM." +license = "MIT" + +[lib] +crate-type = ["lib"] + +[features] +default = ["std", "rand", "getrandom"] +std = ["rand/std", "rand/std_rng"] +minimal_validation = [] + +[dependencies] +rand = { version = "0.8.5", default-features = false, optional = true } +getrandom = { version = "0.2.3", features = ["js"], optional = true } + +[dependencies.secp256k1] +version = "=0.27.0" +default-features = false +features = ["recovery", "lowmemory"] diff --git a/tiny-secp256k1/src/consts.rs b/tiny-secp256k1/src/consts.rs new file mode 100644 index 0000000..0053d5c --- /dev/null +++ b/tiny-secp256k1/src/consts.rs @@ -0,0 +1,18 @@ +pub const PRIVATE_KEY_SIZE: usize = 32; +pub const PUBLIC_KEY_COMPRESSED_SIZE: usize = 33; +pub const PUBLIC_KEY_UNCOMPRESSED_SIZE: usize = 65; +pub const X_ONLY_PUBLIC_KEY_SIZE: usize = 32; +pub const TWEAK_SIZE: usize = 32; +pub const HASH_SIZE: usize = 32; +pub const EXTRA_DATA_SIZE: usize = 32; +pub const SIGNATURE_SIZE: usize = 64; + +pub const ZERO32: [u8; 32] = [0_u8; 32]; +pub const ORDER: [u8; 32] = [ + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 254, 186, 174, 220, + 230, 175, 72, 160, 59, 191, 210, 94, 140, 208, 54, 65, 65, +]; +pub const P_MINUS_N: [u8; 32] = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 69, 81, 35, 25, 80, 183, 95, 196, 64, 45, 161, + 114, 47, 201, 186, 238, +]; diff --git a/tiny-secp256k1/src/context.rs b/tiny-secp256k1/src/context.rs new file mode 100644 index 0000000..6af907d --- /dev/null +++ b/tiny-secp256k1/src/context.rs @@ -0,0 +1,116 @@ +#[cfg(feature = "rand")] +use rand::{self, RngCore}; + +#[allow(clippy::large_stack_arrays)] +mod globals { + pub use secp256k1::{ + ffi::{types::AlignedType, Context}, + AllPreallocated, Secp256k1, + }; + + #[cfg(target_pointer_width = "32")] + pub mod ptr_width_params { + use super::AlignedType; + pub const ALIGN_SIZE: usize = 12; + pub static mut CONTEXT_BUFFER: [AlignedType; ALIGN_SIZE] = [AlignedType::ZERO; ALIGN_SIZE]; + } + #[cfg(target_pointer_width = "64")] + pub mod ptr_width_params { + use super::AlignedType; + pub const ALIGN_SIZE: usize = 13; + pub static mut CONTEXT_BUFFER: [AlignedType; ALIGN_SIZE] = [AlignedType::ZERO; ALIGN_SIZE]; + } + + pub static mut SECP256K1: Option> = None; +} +use globals::{ptr_width_params::CONTEXT_BUFFER, AllPreallocated, Secp256k1, SECP256K1}; + +#[allow(clippy::missing_panics_doc)] +pub fn set_context(seed: &[u8; 32]) -> &'static Secp256k1> { + unsafe { + if SECP256K1.is_none() { + assert_eq!(Secp256k1::preallocate_size(), CONTEXT_BUFFER.len()); + SECP256K1 = Some( + Secp256k1::preallocated_new(&mut CONTEXT_BUFFER) + .expect("CONTEXT_BUFFER length incorrect for this target"), + ); + } + SECP256K1.as_mut().unwrap().seeded_randomize(seed); + SECP256K1.as_ref().unwrap() + } +} + +pub fn get_hcontext() -> &'static Secp256k1> { + unsafe { + if SECP256K1.is_some() { + SECP256K1.as_ref().unwrap() + } else { + #[cfg(feature = "rand")] + { + let mut seed = [0_u8; 32]; + rand::thread_rng().fill_bytes(&mut seed); + set_context(&seed) + } + #[cfg(not(feature = "rand"))] + { + set_context(&simple_rand::get_rand()) + } + } + } +} + +// Simple random seed generator. +// Only for randomizing context when no seed it provided. +#[cfg(not(feature = "rand"))] +mod simple_rand { + struct Xorshift128(u32, u32, u32, u32); + impl Xorshift128 { + const fn new(seed: u32) -> Self { + // Initial state is first 128 bits of + // secp256k1 generator point x value + Self( + 0x79BE_667E ^ seed, + 0xF9DC_BBAC ^ seed.wrapping_shl(13), + 0x55A0_6295 ^ seed.wrapping_shr(7), + 0xCE87_0B07 ^ seed.wrapping_shl(5), + ) + } + fn next_32bytes(&mut self) -> [u8; 32] { + let ret = [0_u32; 8].map(|_| self.next_u32()); + // We know this is safe + unsafe { core::mem::transmute::<[u32; 8], [u8; 32]>(ret) } + } + + fn next_u32(&mut self) -> u32 { + /* Algorithm "xor128" from p. 5 of Marsaglia, "Xorshift RNGs" */ + let mut t = self.3; + + let s = self.0; /* Perform a contrived 32-bit shift. */ + self.3 = self.2; + self.2 = self.1; + self.1 = s; + + t ^= t.wrapping_shl(11); + t ^= t.wrapping_shr(8); + self.0 = t ^ s ^ s.wrapping_shr(19); + self.0 + } + } + + static mut USED: bool = false; + pub fn get_rand() -> [u8; 32] { + // This function returns the same value + // everytime it's called on the same run. + // However, each run should produce a different + // value, so it is suitable (better than not randomizing) + // for the one time initialization of context. + assert!(!unsafe { USED }, "Only use get_rand once!"); + // xorshift128 seeded with ptr of a new stack variable + let ptr = [0_u8; 4].as_ptr() as u32; + let ret = Xorshift128::new(ptr).next_32bytes(); + unsafe { + USED = true; + }; + ret + } +} diff --git a/tiny-secp256k1/src/error.rs b/tiny-secp256k1/src/error.rs new file mode 100644 index 0000000..22703ac --- /dev/null +++ b/tiny-secp256k1/src/error.rs @@ -0,0 +1,31 @@ +#[cfg(not(feature = "minimal_validation"))] +use core::fmt; + +#[derive(Debug)] +#[repr(usize)] +pub enum Error { + BadPrivate = 0_usize, + BadPoint, + BadTweak, + BadHash, + BadSignature, + BadExtraData, + BadParity, + BadRecoveryId, +} + +#[cfg(not(feature = "minimal_validation"))] +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::BadPrivate => f.write_str("Expected Private"), + Self::BadPoint => f.write_str("Expected Point"), + Self::BadTweak => f.write_str("Expected Tweak"), + Self::BadHash => f.write_str("Expected Hash"), + Self::BadSignature => f.write_str("Expected Signature"), + Self::BadExtraData => f.write_str("Expected Extra Data (32 bytes)"), + Self::BadParity => f.write_str("Expected Parity (1 | 0)"), + Self::BadRecoveryId => f.write_str("Bad Recovery Id"), + } + } +} diff --git a/tiny-secp256k1/src/lib.rs b/tiny-secp256k1/src/lib.rs new file mode 100644 index 0000000..8df1787 --- /dev/null +++ b/tiny-secp256k1/src/lib.rs @@ -0,0 +1,439 @@ +#![deny(clippy::all)] +#![deny(clippy::pedantic)] +#![deny(clippy::nursery)] +#![allow(clippy::must_use_candidate)] +#![allow(clippy::module_name_repetitions)] +#![cfg_attr(not(feature = "std"), no_std)] + +//! # tiny-secp256k1 +//! +//! [![NPM](https://img.shields.io/npm/v/tiny-secp256k1.svg)](https://www.npmjs.org/package/tiny-secp256k1) +//! [![docs.rs](https://img.shields.io/docsrs/tiny-secp256k1)](https://docs.rs/tiny-secp256k1/latest/tiny_secp256k1/) +//! +//! This library is under development, and, like the [secp256k1](https://github.com/bitcoin-core/secp256k1) +//! C library (through [secp256k1-sys](https://github.com/rust-bitcoin/rust-secp256k1/) Rust crate) it depends +//! on, this is a research effort to determine an optimal API for end-users of the bitcoinjs ecosystem. +//! +//! ## Examples +//! +//! ### Private keys +//! 32 byte sized slices. +//! ``` +//! use rand::{self, RngCore}; +//! use tiny_secp256k1::{is_private, point_from_scalar, point_add_scalar, Pubkey, PubkeyRef}; +//! +//! let mut privkey = [0_u8; 32]; +//! +//! // 0 is not a valid private key, so this will run at least once (most likely only once) +//! while !is_private(&privkey) { +//! rand::thread_rng().fill_bytes(&mut privkey); +//! } +//! +//! let pkey = point_from_scalar(&privkey, None).unwrap().unwrap(); +//! println!("{:?}", pkey); +//! // Ok(Some(Compressed([3, 126, 249, 27, 122, 231, 178, 211, ...]))) +//! let key = [1_u8; 32]; +//! let pubkey = point_from_scalar(&key, None).unwrap().unwrap(); +//! assert_eq!(pubkey.as_slice(), [3, 27, 132, 197, 86, 123, 18, 100, 64, 153, 93, 62, 213, 170, 186, 5, 101, 215, 30, 24, 52, 96, 72, 25, 255, 156, 23, 245, 233, 213, 221, 7, 143]); +//! ``` + +mod consts; +mod context; +mod error; +mod pubkey; +mod types; +mod utils; +use core::convert::TryInto; + +use context::get_hcontext; +#[doc(inline)] +pub use context::set_context; +pub use error::Error; +pub use pubkey::{Pubkey, PubkeyRef}; +use secp256k1::{ + ecdsa::{RecoverableSignature, Signature}, + schnorr, Parity, Scalar, +}; + +mod validate; +use validate::validate_tweak; + +pub use secp256k1::ecdsa::RecoveryId; +use secp256k1::{KeyPair, Message, PublicKey, SecretKey, XOnlyPublicKey}; + +use consts::{ORDER, P_MINUS_N, X_ONLY_PUBLIC_KEY_SIZE, ZERO32}; +use types::{ + ExtraDataSlice, HashSlice, InvalidInputResult, PrivkeySlice, SignatureSlice, TweakSlice, + XOnlyPubkeySlice, XOnlyPubkeyWithMaybeParity, XOnlyPubkeyWithParity, +}; +use utils::{assume_compression, sign_ecdsa, sign_ecdsa_recoverable}; + +pub fn is_point(pubkey: &PubkeyRef) -> bool { + let len = pubkey.len(); + if len == X_ONLY_PUBLIC_KEY_SIZE { + XOnlyPublicKey::from_slice(pubkey.as_slice()).map_or_else(|_error| false, |_pk| true) + } else { + PublicKey::from_slice(pubkey.as_slice()).map_or_else(|_error| false, |_pk| true) + } +} + +pub fn is_private(v: &PrivkeySlice) -> bool { + v > &ZERO32 && v < &ORDER +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey1` or `pubkey2` is invalid. +pub fn point_add( + pubkey1: &PubkeyRef, + pubkey2: &PubkeyRef, + compressed: Option, +) -> InvalidInputResult> { + let compressed = assume_compression(compressed, Some(pubkey1.len())); + let key1 = PublicKey::from_slice(pubkey1.as_slice()).map_err(|_| Error::BadPoint)?; + let key2 = PublicKey::from_slice(pubkey2.as_slice()).map_err(|_| Error::BadPoint)?; + + Ok(key1.combine(&key2).map_or_else( + |_| None, + |v| { + Some(if compressed == 33 { + Pubkey::Compressed(v.serialize()) + } else { + Pubkey::Uncompressed(v.serialize_uncompressed()) + }) + }, + )) +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey` is invalid. +/// `Error::BadTweak` returned if `tweak` is invalid. +/// +/// # Panics +/// If tweak is invalid. +pub fn point_add_scalar( + pubkey: &PubkeyRef, + tweak: &TweakSlice, + compressed: Option, +) -> InvalidInputResult> { + let outputlen = assume_compression(compressed, Some(pubkey.len())); + let key = PublicKey::from_slice(pubkey.as_slice()).map_err(|_| Error::BadPoint)?; + validate_tweak(tweak)?; + Ok(key + .add_exp_tweak( + get_hcontext(), + &Scalar::from_be_bytes(*tweak).expect("Proper length checked"), + ) + .map_or_else( + |_| None, + |new_key| { + Some(if outputlen == 33 { + Pubkey::Compressed(new_key.serialize()) + } else { + Pubkey::Uncompressed(new_key.serialize_uncompressed()) + }) + }, + )) +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey` is invalid. +/// `Error::BadTweak` returned if `tweak` is invalid. +/// +/// # Panics +/// If tweak is invalid. +pub fn x_only_point_add_tweak( + pubkey: &XOnlyPubkeySlice, + tweak: &TweakSlice, +) -> InvalidInputResult> { + let key = XOnlyPublicKey::from_slice(pubkey).map_err(|_| Error::BadPoint)?; + validate_tweak(tweak)?; + let parity = key.add_tweak( + get_hcontext(), + &Scalar::from_be_bytes(*tweak).expect("Proper length checked"), + ); + if let Ok((key, parity)) = parity { + Ok(Some((key.serialize(), parity.to_i32()))) + } else { + Ok(None) + } +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey` or `result` is invalid. +/// `Error::BadTweak` returned if `tweak` is invalid. +/// `Error::BadParity` returned if `result` has `parity` and it is invalid. +/// +/// # Panics +/// If tweak is invalid. +pub fn x_only_point_add_tweak_check( + pubkey: &XOnlyPubkeySlice, + result: &XOnlyPubkeyWithMaybeParity, + tweak: &TweakSlice, +) -> InvalidInputResult { + // Currently there is almost no difference between + // secp256k1_xonly_pubkey_tweak_add_check and doing it over and checking equality. + // Later on, performance gains might be added for having parity, so we implement it. + if let Some(parity) = result.1 { + let pubkey = XOnlyPublicKey::from_slice(pubkey).map_err(|_| Error::BadPoint)?; + let result = XOnlyPublicKey::from_slice(&result.0).map_err(|_| Error::BadPoint)?; + let parity = Parity::from_i32(parity).map_err(|_| Error::BadParity)?; + Ok(pubkey.tweak_add_check( + get_hcontext(), + &result, + parity, + Scalar::from_be_bytes(*tweak).expect("Proper length checked"), + )) + } else { + x_only_point_add_tweak(pubkey, tweak)?.map_or(Ok(false), |v| Ok(v.0 == result.0)) + } +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey` is invalid. +pub fn point_compress(pubkey: &PubkeyRef, compressed: Option) -> InvalidInputResult { + let outputlen = assume_compression(compressed, Some(pubkey.len())); + let key = PublicKey::from_slice(pubkey.as_slice()).map_err(|_| Error::BadPoint)?; + Ok(if outputlen == 33 { + Pubkey::Compressed(key.serialize()) + } else { + Pubkey::Uncompressed(key.serialize_uncompressed()) + }) +} + +/// # Errors +/// `Error::BadPrivate` returned if `private` is invalid. +pub fn point_from_scalar( + private: &PrivkeySlice, + compressed: Option, +) -> InvalidInputResult> { + let compressed = assume_compression(compressed, None); + let pk = SecretKey::from_slice(private).map_err(|_| Error::BadPrivate)?; + let pb = PublicKey::from_secret_key(get_hcontext(), &pk); + Ok(Some(if compressed == 33 { + Pubkey::Compressed(pb.serialize()) + } else { + Pubkey::Uncompressed(pb.serialize_uncompressed()) + })) +} + +/// # Errors +/// `Error::BadPrivate` returned if `private` is invalid. +#[allow(clippy::missing_panics_doc)] +pub fn x_only_point_from_scalar( + private: &PrivkeySlice, +) -> InvalidInputResult { + let pk = SecretKey::from_slice(private).map_err(|_| Error::BadPrivate)?; + let pb = PublicKey::from_secret_key(get_hcontext(), &pk); + let ser = pb.serialize(); + Ok(( + ser[1..33].try_into().expect("32 bytes"), + i32::from(ser[0] & 1), + )) +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey` is invalid. +#[allow(clippy::missing_panics_doc)] +pub fn x_only_point_from_point(pubkey: &PubkeyRef) -> InvalidInputResult { + let pb = PublicKey::from_slice(pubkey.as_slice()).map_err(|_| Error::BadPoint)?; + let ser = pb.serialize(); + Ok(( + ser[1..33].try_into().expect("32 bytes"), + i32::from(ser[0] & 1), + )) +} + +/// # Errors +/// `Error::BadPoint` returned if `pubkey` is invalid. +/// `Error::BadTweak` returned if `tweak` is invalid. +/// +/// # Panics +/// If tweak is invalid. +pub fn point_multiply( + pubkey: &PubkeyRef, + tweak: &TweakSlice, + compressed: Option, +) -> InvalidInputResult> { + let outputlen = assume_compression(compressed, Some(pubkey.len())); + let pb = PublicKey::from_slice(pubkey.as_slice()).map_err(|_| Error::BadPoint)?; + validate_tweak(tweak)?; + pb.mul_tweak( + get_hcontext(), + &Scalar::from_be_bytes(*tweak).expect("Proper length checked"), + ) + .map_or(Ok(None), |new_pb| { + Ok(Some(if outputlen == 33 { + Pubkey::Compressed(new_pb.serialize()) + } else { + Pubkey::Uncompressed(new_pb.serialize_uncompressed()) + })) + }) +} + +/// # Errors +/// `Error::BadPrivate` returned if `private` is invalid. +/// `Error::BadTweak` returned if `tweak` is invalid. +/// +/// # Panics +/// If tweak is invalid. +pub fn private_add( + private: &PrivkeySlice, + tweak: &TweakSlice, +) -> InvalidInputResult> { + validate_tweak(tweak)?; + let sec = SecretKey::from_slice(private.as_slice()).map_err(|_| Error::BadPrivate)?; + sec.add_tweak(&Scalar::from_be_bytes(*tweak).expect("Proper length checked")) + .map_or(Ok(None), |new_sec| Ok(Some(new_sec.secret_bytes()))) +} + +/// # Errors +/// `Error::BadPrivate` returned if `private` is invalid. +/// `Error::BadTweak` returned if `tweak` is invalid. +/// +/// # Panics +/// If tweak is invalid. +pub fn private_sub( + private: &PrivkeySlice, + tweak: &TweakSlice, +) -> InvalidInputResult> { + validate_tweak(tweak)?; + // If tweak is 0, x - 0 = x. Also, SecretKey::from_slice will error + // if we try to use 0. + if tweak == &ZERO32 { + return Ok(Some(*private)); + } + let sec = SecretKey::from_slice(private.as_slice()).map_err(|_| Error::BadPrivate)?; + + // We now know tweak is a valid SecretKey (validate_tweak checks < N, guard clause above checks 0) + let mut tweak = SecretKey::from_slice(tweak.as_slice()).map_err(|_| Error::BadPrivate)?; + tweak = tweak.negate(); + + sec.add_tweak(&Scalar::from_be_bytes(tweak.secret_bytes()).expect("Proper length checked")) + .map_or(Ok(None), |new_sec| Ok(Some(new_sec.secret_bytes()))) +} + +/// # Errors +/// `Error::BadPrivate` returned if `private` is invalid. +pub fn private_negate(private: &PrivkeySlice) -> InvalidInputResult { + let mut sec = SecretKey::from_slice(private.as_slice()).map_err(|_| Error::BadPrivate)?; + sec = sec.negate(); + Ok(sec.secret_bytes()) +} + +/// # Errors +/// `Error::BadHash` returned if `hash` is invalid. +/// `Error::BadPrivate` returned if `private` is invalid. +pub fn sign( + hash: &HashSlice, + private: &PrivkeySlice, + extra_data: Option<&ExtraDataSlice>, +) -> InvalidInputResult { + let sec = SecretKey::from_slice(private.as_slice()).map_err(|_| Error::BadPrivate)?; + let msg = Message::from_slice(hash.as_slice()).map_err(|_| Error::BadHash)?; + let secp = get_hcontext(); + let sig = sign_ecdsa(secp, msg, sec, extra_data); + Ok(sig.serialize_compact()) +} + +/// # Errors +/// `Error::BadHash` returned if `hash` is invalid. +/// `Error::BadPrivate` returned if `private` is invalid. +pub fn sign_recoverable( + hash: &HashSlice, + private: &PrivkeySlice, + extra_data: Option<&ExtraDataSlice>, +) -> InvalidInputResult<(RecoveryId, SignatureSlice)> { + let sec = SecretKey::from_slice(private.as_slice()).map_err(|_| Error::BadPrivate)?; + let msg = Message::from_slice(hash.as_slice()).map_err(|_| Error::BadHash)?; + let secp = get_hcontext(); + let sig = sign_ecdsa_recoverable(secp, msg, sec, extra_data); + Ok(sig.serialize_compact()) +} + +/// # Errors +/// `Error::BadHash` returned if `hash` is invalid. +/// `Error::BadSignature` returned if `sig` is invalid. +/// `Error::BadRecoveryId` returned if `recovery_id` is invalid. +pub fn recover( + hash: &HashSlice, + sig: &SignatureSlice, + recovery_id: RecoveryId, + compressed: Option, +) -> InvalidInputResult { + let outputlen = assume_compression(compressed, None); + // Check that the r value is less than P - N when 2nd bit is set + if recovery_id.to_i32() & 2 == 2 && &sig[..32] >= &P_MINUS_N { + return Err(Error::BadRecoveryId); + } + let msg = Message::from_slice(hash.as_slice()).map_err(|_| Error::BadHash)?; + let sig = RecoverableSignature::from_compact(sig.as_slice(), recovery_id) + .map_err(|_| Error::BadSignature)?; + + let secp = get_hcontext(); + let pubkey = secp + .recover_ecdsa(&msg, &sig) + .map_err(|_| Error::BadSignature)?; + Ok(if outputlen == 33 { + Pubkey::Compressed(pubkey.serialize()) + } else { + Pubkey::Uncompressed(pubkey.serialize_uncompressed()) + }) +} + +/// # Errors +/// `Error::BadHash` returned if hash is invalid. +/// `Error::BadPrivate` returned if private is invalid. +pub fn sign_schnorr( + hash: &HashSlice, + private: &PrivkeySlice, + extra_data: Option<&ExtraDataSlice>, +) -> InvalidInputResult { + let secp = get_hcontext(); + let kp = KeyPair::from_seckey_slice(secp, private).map_err(|_| Error::BadPrivate)?; + let msg = Message::from_slice(hash).map_err(|_| Error::BadHash)?; + let nonce = extra_data.map_or(&ZERO32, |v| v); + let sig = secp.sign_schnorr_with_aux_rand(&msg, &kp, nonce); + Ok(*sig.as_ref()) +} + +/// # Errors +/// `Error::BadHash` returned if hash is invalid. +/// `Error::BadPoint` returned if pubkey is invalid. +/// `Error::BadSignature` returned if sig is invalid. +pub fn verify( + hash: &HashSlice, + pubkey: &PubkeyRef, + sig: &SignatureSlice, + strict: Option, +) -> InvalidInputResult { + let pb = PublicKey::from_slice(pubkey.as_slice()).map_err(|_| Error::BadPoint)?; + let msg = Message::from_slice(hash.as_slice()).map_err(|_| Error::BadHash)?; + let mut sg = Signature::from_compact(sig.as_slice()).map_err(|_| Error::BadSignature)?; + + let secp = get_hcontext(); + + if !strict.unwrap_or(false) { + sg.normalize_s(); + } + + Ok(secp.verify_ecdsa(&msg, &sg, &pb).is_ok()) +} + +/// # Errors +/// `Error::BadHash` returned if hash is invalid. +/// `Error::BadPoint` returned if pubkey is invalid. +/// `Error::BadSignature` returned if signature is invalid. +pub fn verify_schnorr( + hash: &HashSlice, + pubkey: &XOnlyPubkeySlice, + signature: &SignatureSlice, +) -> InvalidInputResult { + let pb = XOnlyPublicKey::from_slice(pubkey.as_slice()).map_err(|_| Error::BadPoint)?; + let msg = Message::from_slice(hash.as_slice()).map_err(|_| Error::BadHash)?; + let sg = + schnorr::Signature::from_slice(signature.as_slice()).map_err(|_| Error::BadSignature)?; + + let secp = get_hcontext(); + + Ok(secp.verify_schnorr(&sg, &msg, &pb).is_ok()) +} diff --git a/tiny-secp256k1/src/pubkey.rs b/tiny-secp256k1/src/pubkey.rs new file mode 100644 index 0000000..c6bd289 --- /dev/null +++ b/tiny-secp256k1/src/pubkey.rs @@ -0,0 +1,83 @@ +#[derive(Debug)] +pub enum PubkeyRef<'a> { + XOnly(&'a [u8; 32]), + Compressed(&'a [u8; 33]), + Uncompressed(&'a [u8; 65]), +} + +#[derive(Debug)] +pub enum Pubkey { + XOnly([u8; 32]), + Compressed([u8; 33]), + Uncompressed([u8; 65]), +} + +impl Pubkey { + #[allow(clippy::missing_panics_doc)] + pub fn new_from_len(size: usize) -> Self { + match size { + 32 => Self::XOnly([0_u8; 32]), + 33 => Self::Compressed([0_u8; 33]), + 65 => Self::Uncompressed([0_u8; 65]), + _ => panic!("invalid length"), + } + } + + pub fn as_mut_ptr(&mut self) -> *mut u8 { + match self { + Self::XOnly(v) => v.as_mut_ptr(), + Self::Compressed(v) => v.as_mut_ptr(), + Self::Uncompressed(v) => v.as_mut_ptr(), + } + } +} + +macro_rules! impl_pubkey { + ($name:ident, $( $lt:tt )?) => { + impl$(<$lt>)? $name$(<$lt>)? { + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + match self { + Self::XOnly(_) => 32, + Self::Compressed(_) => 33, + Self::Uncompressed(_) => 65, + } + } + + pub const fn as_ptr(&self) -> *const u8 { + match self { + Self::XOnly(v) => v.as_ptr(), + Self::Compressed(v) => v.as_ptr(), + Self::Uncompressed(v) => v.as_ptr(), + } + } + + pub fn as_slice(&self) -> &[u8] { + match self { + Self::XOnly(v) => &v[..], + Self::Compressed(v) => &v[..], + Self::Uncompressed(v) => &v[..], + } + } + } + }; +} + +macro_rules! impl_pubkey_from { + ($type:ty, $name:ident, $variant:ident, $( $lt:tt )?) => { + impl$(<$lt>)? From<$type> for $name$(<$lt>)? { + fn from(v: $type) -> Self { + Self::$variant(v) + } + } + }; +} + +impl_pubkey!(PubkeyRef, 'a); +impl_pubkey!(Pubkey,); +impl_pubkey_from!(&'a [u8; 32], PubkeyRef, XOnly, 'a); +impl_pubkey_from!(&'a [u8; 33], PubkeyRef, Compressed, 'a); +impl_pubkey_from!(&'a [u8; 65], PubkeyRef, Uncompressed, 'a); +impl_pubkey_from!([u8; 32], Pubkey, XOnly,); +impl_pubkey_from!([u8; 33], Pubkey, Compressed,); +impl_pubkey_from!([u8; 65], Pubkey, Uncompressed,); diff --git a/tiny-secp256k1/src/types.rs b/tiny-secp256k1/src/types.rs new file mode 100644 index 0000000..2f6f779 --- /dev/null +++ b/tiny-secp256k1/src/types.rs @@ -0,0 +1,18 @@ +pub use super::{ + consts::{ + EXTRA_DATA_SIZE, HASH_SIZE, PRIVATE_KEY_SIZE, SIGNATURE_SIZE, TWEAK_SIZE, + X_ONLY_PUBLIC_KEY_SIZE, + }, + error::Error, +}; + +pub type InvalidInputResult = Result; + +pub type PrivkeySlice = [u8; PRIVATE_KEY_SIZE]; +pub type XOnlyPubkeySlice = [u8; X_ONLY_PUBLIC_KEY_SIZE]; +pub type XOnlyPubkeyWithMaybeParity = (XOnlyPubkeySlice, Option); +pub type XOnlyPubkeyWithParity = (XOnlyPubkeySlice, i32); +pub type TweakSlice = [u8; TWEAK_SIZE]; +pub type HashSlice = [u8; HASH_SIZE]; +pub type ExtraDataSlice = [u8; EXTRA_DATA_SIZE]; +pub type SignatureSlice = [u8; SIGNATURE_SIZE]; diff --git a/tiny-secp256k1/src/utils.rs b/tiny-secp256k1/src/utils.rs new file mode 100644 index 0000000..815b10e --- /dev/null +++ b/tiny-secp256k1/src/utils.rs @@ -0,0 +1,103 @@ +use crate::{ + consts::{PUBLIC_KEY_COMPRESSED_SIZE, PUBLIC_KEY_UNCOMPRESSED_SIZE}, + types::{ExtraDataSlice, SignatureSlice, SIGNATURE_SIZE}, +}; +use secp256k1::{ + ecdsa::{RecoverableSignature, RecoveryId, Signature}, + ffi::{self, CPtr}, + AllPreallocated, Message, Secp256k1, SecretKey, +}; + +pub fn assume_compression(compressed: Option, p: Option) -> usize { + // To allow for XOnly PubkeyRef length to indicate compressed, + // We bitwise OR 1 (32 -> 33, while 33 and 65 stay unchanged) + compressed.map_or_else( + || p.map_or(PUBLIC_KEY_COMPRESSED_SIZE, |v| v | 1), + |v| { + if v { + PUBLIC_KEY_COMPRESSED_SIZE + } else { + PUBLIC_KEY_UNCOMPRESSED_SIZE + } + }, + ) +} + +// TODO: Get ecdsa sign and sign_recoverable to accept extra entropy for rfc6979 +pub fn sign_ecdsa( + secp: &'static Secp256k1>, + msg: Message, + sec: SecretKey, + extra_data: Option<&ExtraDataSlice>, +) -> Signature { + unsafe { + let mut sig = ffi::Signature::new(); + let noncedata = extra_data + .map_or(core::ptr::null(), |v| v.as_ptr()) + .cast::(); + + assert_eq!( + ffi::secp256k1_ecdsa_sign( + secp.ctx().as_ptr(), + &mut sig, + msg.as_c_ptr(), + sec.as_c_ptr(), + ffi::secp256k1_nonce_function_rfc6979, + noncedata + ), + 1 + ); + + let mut output: SignatureSlice = [0_u8; SIGNATURE_SIZE]; + assert_eq!( + ffi::secp256k1_ecdsa_signature_serialize_compact( + ffi::secp256k1_context_no_precomp, + output.as_mut_ptr(), + &sig, + ), + 1 + ); + Signature::from_compact(&output).unwrap() + } +} + +// TODO: Get ecdsa sign and sign_recoverable to accept extra entropy for rfc6979 +pub fn sign_ecdsa_recoverable( + secp: &'static Secp256k1>, + msg: Message, + sec: SecretKey, + extra_data: Option<&ExtraDataSlice>, +) -> RecoverableSignature { + unsafe { + let mut sig = ffi::recovery::RecoverableSignature::new(); + let noncedata = extra_data + .map_or(core::ptr::null(), |v| v.as_ptr()) + .cast::(); + + assert_eq!( + ffi::recovery::secp256k1_ecdsa_sign_recoverable( + secp.ctx().as_ptr(), + &mut sig, + msg.as_c_ptr(), + sec.as_c_ptr(), + ffi::secp256k1_nonce_function_rfc6979, + noncedata + ), + 1 + ); + + let mut output: SignatureSlice = [0_u8; SIGNATURE_SIZE]; + let mut recid: i32 = 0; + + assert_eq!( + ffi::recovery::secp256k1_ecdsa_recoverable_signature_serialize_compact( + ffi::secp256k1_context_no_precomp, + output.as_mut_ptr(), + &mut recid, + &sig, + ), + 1 + ); + RecoverableSignature::from_compact(&output, RecoveryId::from_i32(recid).unwrap()).unwrap() + } +} diff --git a/tiny-secp256k1/src/validate.rs b/tiny-secp256k1/src/validate.rs new file mode 100644 index 0000000..8f7ffcd --- /dev/null +++ b/tiny-secp256k1/src/validate.rs @@ -0,0 +1,63 @@ +use super::{ + consts::ORDER, + error::Error, + types::{InvalidInputResult, TweakSlice}, +}; + +// pub(crate) fn is_zero(v: &[u8; 32]) -> bool { +// v == &ZERO32 +// } + +// pub(crate) fn is_der_point(v: &PubkeySlice) -> bool { +// v.1 == PUBLIC_KEY_COMPRESSED_SIZE || v.1 == PUBLIC_KEY_UNCOMPRESSED_SIZE +// } + +// pub(crate) fn is_point_compressed(v: &PubkeySlice) -> bool { +// v.1 == PUBLIC_KEY_COMPRESSED_SIZE +// } + +pub fn is_tweak(v: &TweakSlice) -> bool { + v < &ORDER +} + +// pub(crate) fn is_signature(v: &SignatureSlice) -> bool { +// // guarantee size of array +// let _: &[u8; 64] = v; +// unsafe { +// let r = v.as_ptr().cast::<[u8; 32]>().as_ref().unwrap(); +// let s = v.as_ptr().offset(32).cast::<[u8; 32]>().as_ref().unwrap(); +// is_tweak(r) && is_tweak(s) +// } +// } + +// pub(crate) fn validate_parity(p: i32) -> InvalidInputResult<()> { +// if p != 0 && p != 1 { +// Err(Error::BadParity) +// } else { +// Ok(()) +// } +// } + +// pub(crate) fn validate_private(p: &PrivkeySlice) -> InvalidInputResult<()> { +// if is_private(p) { +// Ok(()) +// } else { +// Err(Error::BadPrivate) +// } +// } + +pub fn validate_tweak(p: &TweakSlice) -> InvalidInputResult<()> { + if is_tweak(p) { + Ok(()) + } else { + Err(Error::BadTweak) + } +} + +// pub(crate) fn validate_signature(p: &SignatureSlice) -> InvalidInputResult<()> { +// if is_signature(p) { +// Ok(()) +// } else { +// Err(Error::BadSignature) +// } +// }