diff --git a/.circleci/config.yml b/.circleci/config.yml index 6405ace61d..bbdded09c8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,7 @@ workflows: test: # Keep those job names in sync with .mergify.yml jobs: + - package_crypto - package_schema - package_std - package_storage @@ -23,18 +24,20 @@ workflows: requires: - package_vm - package_vm_cranelift + - package_crypto filters: branches: only: # Long living branches - main - /^[0-9]+\.[0-9]+$/ - # 👇 Add your branch here if benchmarking matters to your work + # 👇Add your branch here if benchmarking matters to your work - benchmarking - update-wasmer - metering-restart - load-wasm-speed - cache-analyze + - crypto-verify-benchmarks deploy: jobs: - build_and_upload_devcontracts: @@ -45,6 +48,33 @@ workflows: ignore: /.*/ jobs: + package_crypto: + docker: + - image: rust:1.49.0 + steps: + - checkout + - run: + name: Version information + command: rustc --version; cargo --version; rustup --version; rustup target list --installed + - restore_cache: + keys: + - cargocache-v2-package_crypto-rust:1.49.0-{{ checksum "Cargo.lock" }} + - run: + name: Build + working_directory: ~/project/packages/crypto + command: cargo build --locked + - run: + name: Run tests + working_directory: ~/project/packages/crypto + command: cargo test --locked + - save_cache: + paths: + - /usr/local/cargo/registry + - target/debug/.fingerprint + - target/debug/build + - target/debug/deps + key: cargocache-v2-package_crypto-rust:1.49.0-{{ checksum "Cargo.lock" }} + package_schema: docker: - image: rust:1.49.0 @@ -858,13 +888,17 @@ jobs: keys: - cargocache-v2-benchmarking-rust:1.49.0-{{ checksum "Cargo.lock" }} - run: - name: Run benchmarks (Singlepass) + name: Run vm benchmarks (Singlepass) working_directory: ~/project/packages/vm command: cargo bench --no-default-features -- --color never --save-baseline singlepass - run: - name: Run benchmarks (Cranelift) + name: Run vm benchmarks (Cranelift) working_directory: ~/project/packages/vm command: cargo bench --no-default-features --features cranelift -- --color never --save-baseline cranelift + - run: + name: Run crypto benchmarks + working_directory: ~/project/packages/crypto + command: cargo bench -- --color never --save-baseline crypto - save_cache: paths: - /usr/local/cargo/registry diff --git a/Cargo.lock b/Cargo.lock index 3c9d7572db..c807f2cbea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -201,9 +201,11 @@ name = "cosmwasm-crypto" version = "0.13.2" dependencies = [ "base64", + "criterion", "digest", "ed25519-zebra", "elliptic-curve", + "english-numbers", "hex", "hex-literal", "k256", @@ -612,6 +614,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "english-numbers" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4f5d6e192964d498b45abee72ca445e91909094bc8e8791259e82c2a0d1aa6" + [[package]] name = "enumset" version = "1.0.4" diff --git a/packages/crypto/Cargo.toml b/packages/crypto/Cargo.toml index 2d25ac30d4..1f47c830dd 100644 --- a/packages/crypto/Cargo.toml +++ b/packages/crypto/Cargo.toml @@ -14,6 +14,10 @@ default = [] # This feature requires Rust nightly because it depends on the unstable backtrace feature. backtraces = [] +[lib] +# See https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options +bench = false + [dependencies] k256 = { version = "0.7.2", features = ["ecdsa"] } ed25519-zebra = "2" @@ -22,10 +26,16 @@ rand_core = { version = "0.5", features = ["getrandom"] } thiserror = "1.0" [dev-dependencies] +criterion = "0.3" serde = { version = "1.0.103", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" sha2 = "0.9" base64 = "0.13.0" hex = "0.4" hex-literal = "0.3.1" +english-numbers = "0.3" elliptic-curve = "0.8.4" + +[[bench]] +name = "main" +harness = false diff --git a/packages/crypto/README.md b/packages/crypto/README.md index b7f72daaad..b1dc660d0e 100644 --- a/packages/crypto/README.md +++ b/packages/crypto/README.md @@ -15,6 +15,13 @@ and [cosmwasm-std](`https://crates.io/crates/cosmwasm-std`) crates. - `ed25519_batch_verify()`: Batch digital signature verification using the EdDSA ed25519 scheme, for Tendemint signature / public key formats. +## Benchmarking + +``` +cd packages/crypto +cargo bench +``` + ## License This package is part of the cosmwasm repository, licensed under the Apache diff --git a/packages/crypto/benches/main.rs b/packages/crypto/benches/main.rs new file mode 100644 index 0000000000..1a0ee74c7f --- /dev/null +++ b/packages/crypto/benches/main.rs @@ -0,0 +1,188 @@ +use criterion::{criterion_group, criterion_main, Criterion, PlottingBackend}; +use std::time::Duration; + +use english_numbers::convert_no_fmt; +use hex_literal::hex; +use serde::Deserialize; + +// Crypto stuff +use digest::Digest; +use elliptic_curve::sec1::ToEncodedPoint; +use k256::ecdsa::SigningKey; // type alias +use sha2::Sha256; + +use cosmwasm_crypto::{ + ed25519_batch_verify, ed25519_verify, secp256k1_recover_pubkey, secp256k1_verify, +}; +use std::cmp::min; + +const COSMOS_SECP256K1_MSG_HEX: &str = "0a93010a90010a1c2f636f736d6f732e62616e6b2e763162657461312e4d736753656e6412700a2d636f736d6f7331706b707472653766646b6c366766727a6c65736a6a766878686c63337234676d6d6b38727336122d636f736d6f7331717970717870713971637273737a673270767871367273307a716733797963356c7a763778751a100a0575636f736d12073132333435363712650a4e0a460a1f2f636f736d6f732e63727970746f2e736563703235366b312e5075624b657912230a21034f04181eeba35391b858633a765c4a0c189697b40d216354d50890d350c7029012040a02080112130a0d0a0575636f736d12043230303010c09a0c1a0c73696d642d74657374696e672001"; +const COSMOS_SECP256K1_SIGNATURE_HEX: &str = "c9dd20e07464d3a688ff4b710b1fbc027e495e797cfa0b4804da2ed117959227772de059808f765aa29b8f92edf30f4c2c5a438e30d3fe6897daa7141e3ce6f9"; +const COSMOS_SECP256K1_PUBKEY_BASE64: &str = "A08EGB7ro1ORuFhjOnZcSgwYlpe0DSFjVNUIkNNQxwKQ"; + +// TEST 3 test vector from https://tools.ietf.org/html/rfc8032#section-7.1 +const COSMOS_ED25519_MSG_HEX: &str = "af82"; +const COSMOS_ED25519_SIGNATURE_HEX: &str = "6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"; +const COSMOS_ED25519_PUBLIC_KEY_HEX: &str = + "fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025"; + +// Test data from https://tools.ietf.org/html/rfc8032#section-7.1 +const COSMOS_ED25519_TESTS_JSON: &str = "./testdata/ed25519_tests.json"; + +#[derive(Deserialize, Debug)] +struct Encoded { + #[serde(rename = "privkey")] + private_key: String, + #[serde(rename = "pubkey")] + public_key: String, + message: String, + signature: String, +} + +fn read_cosmos_sigs() -> Vec { + use std::fs::File; + use std::io::BufReader; + + // Open the file in read-only mode with buffer. + let file = File::open(COSMOS_ED25519_TESTS_JSON).unwrap(); + let reader = BufReader::new(file); + + serde_json::from_reader(reader).unwrap() +} + +fn read_decode_cosmos_sigs() -> (Vec>, Vec>, Vec>) { + let codes = read_cosmos_sigs(); + + let mut messages: Vec> = vec![]; + let mut signatures: Vec> = vec![]; + let mut public_keys: Vec> = vec![]; + + for encoded in codes { + let message = hex::decode(&encoded.message).unwrap(); + messages.push(message); + + let signature = hex::decode(&encoded.signature).unwrap(); + signatures.push(signature); + + let public_key = hex::decode(&encoded.public_key).unwrap(); + public_keys.push(public_key); + } + + (messages, signatures, public_keys) +} + +fn bench_crypto(c: &mut Criterion) { + let mut group = c.benchmark_group("Crypto"); + + group.bench_function("secp256k1_verify", |b| { + let message = hex::decode(COSMOS_SECP256K1_MSG_HEX).unwrap(); + let message_hash = Sha256::digest(&message); + let signature = hex::decode(COSMOS_SECP256K1_SIGNATURE_HEX).unwrap(); + let public_key = base64::decode(COSMOS_SECP256K1_PUBKEY_BASE64).unwrap(); + b.iter(|| { + assert!(secp256k1_verify(&message_hash, &signature, &public_key).unwrap()); + }); + }); + + group.bench_function("secp256k1_recover_pubkey", |b| { + let message_hash = + hex!("82ff40c0a986c6a5cfad4ddf4c3aa6996f1a7837f9c398e17e5de5cbd5a12b28"); + let private_key = + hex!("3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1"); + let r_s = hex!("99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9129ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca66"); + let recovery_param: u8 = 0; + + let expected = SigningKey::from_bytes(&private_key) + .unwrap() + .verify_key() + .to_encoded_point(false) + .as_bytes() + .to_vec(); + + b.iter(|| { + let pubkey = secp256k1_recover_pubkey(&message_hash, &r_s, recovery_param).unwrap(); + assert_eq!(pubkey, expected); + }); + }); + + group.bench_function("ed25519_verify", |b| { + let message = hex::decode(COSMOS_ED25519_MSG_HEX).unwrap(); + let signature = hex::decode(COSMOS_ED25519_SIGNATURE_HEX).unwrap(); + let public_key = hex::decode(COSMOS_ED25519_PUBLIC_KEY_HEX).unwrap(); + b.iter(|| { + assert!(ed25519_verify(&message, &signature, &public_key).unwrap()); + }); + }); + + // Ed25519 batch verification of different batch lengths + { + let (messages, signatures, public_keys) = read_decode_cosmos_sigs(); + let messages: Vec<&[u8]> = messages.iter().map(|m| m.as_slice()).collect(); + let signatures: Vec<&[u8]> = signatures.iter().map(|m| m.as_slice()).collect(); + let public_keys: Vec<&[u8]> = public_keys.iter().map(|m| m.as_slice()).collect(); + + for n in (1..=min(messages.len(), 10)).step_by(2) { + group.bench_function( + format!("ed25519_batch_verify_{}", convert_no_fmt(n as i64)), + |b| { + b.iter(|| { + assert!(ed25519_batch_verify( + &messages[..n], + &signatures[..n], + &public_keys[..n] + ) + .unwrap()); + }); + }, + ); + } + } + + // Ed25519 batch verification of different batch lengths, with the same pubkey + { + //FIXME: Use different messages / signatures + let messages = [hex::decode(COSMOS_ED25519_MSG_HEX).unwrap()]; + let signatures = [hex::decode(COSMOS_ED25519_SIGNATURE_HEX).unwrap()]; + let public_keys = [hex::decode(COSMOS_ED25519_PUBLIC_KEY_HEX).unwrap()]; + + let messages: Vec<&[u8]> = messages.iter().map(|m| m.as_slice()).collect(); + let signatures: Vec<&[u8]> = signatures.iter().map(|m| m.as_slice()).collect(); + let public_keys: Vec<&[u8]> = public_keys.iter().map(|m| m.as_slice()).collect(); + + for n in (1..10).step_by(2) { + group.bench_function( + format!( + "ed25519_batch_verify_one_pubkey_{}", + convert_no_fmt(n as i64) + ), + |b| { + b.iter(|| { + assert!(ed25519_batch_verify( + &messages.repeat(n), + &signatures.repeat(n), + &public_keys + ) + .unwrap()); + }); + }, + ); + } + } + + group.finish(); +} + +fn make_config() -> Criterion { + Criterion::default() + .plotting_backend(PlottingBackend::Plotters) + .without_plots() + .measurement_time(Duration::new(10, 0)) + .sample_size(12) +} + +criterion_group!( + name = crypto; + config = make_config(); + targets = bench_crypto +); +criterion_main!(crypto); diff --git a/packages/crypto/src/secp256k1.rs b/packages/crypto/src/secp256k1.rs index dd9e9d1097..1089ad2297 100644 --- a/packages/crypto/src/secp256k1.rs +++ b/packages/crypto/src/secp256k1.rs @@ -206,7 +206,7 @@ mod tests { .unwrap()); // Wrong message fails - let bad_message_hash = Sha256::new().chain([MSG, "\0"].concat()).finalize(); + let bad_message_hash = Sha256::new().chain(MSG).chain("\0").finalize(); assert!(!secp256k1_verify( &bad_message_hash, signature.as_bytes(), @@ -245,7 +245,7 @@ mod tests { let signature = hex::decode(sig).unwrap(); // Explicit hash - let message_hash = Sha256::new().chain(&message).finalize(); + let message_hash = Sha256::digest(&message); // secp256k1_verify works assert!( @@ -281,7 +281,7 @@ mod tests { let message = hex::decode(&encoded.message).unwrap(); let hash = hex::decode(&encoded.message_hash).unwrap(); - let message_hash = Sha256::new().chain(&message).finalize(); + let message_hash = Sha256::digest(&message); assert_eq!(hash.as_slice(), message_hash.as_slice()); let signature = hex::decode(&encoded.signature).unwrap();