diff --git a/Cargo.lock b/Cargo.lock index 427c0cc78..b1dee3359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -496,6 +496,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "ecsm" version = "0.1.0" @@ -586,6 +597,7 @@ name = "executor" version = "0.1.0" dependencies = [ "ecsm", + "k256", "rustc-demangle", "serde", "serde_json", @@ -848,6 +860,8 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ + "critical-section", + "dlmalloc", "embedded-alloc", "getrandom 0.2.16", "getrandom 0.3.4", diff --git a/bench_vs/lambda/recursion/Cargo.lock b/bench_vs/lambda/recursion/Cargo.lock index bf31738e2..2b3411743 100644 --- a/bench_vs/lambda/recursion/Cargo.lock +++ b/bench_vs/lambda/recursion/Cargo.lock @@ -233,6 +233,7 @@ name = "executor" version = "0.1.0" dependencies = [ "ecsm", + "k256", "rustc-demangle", "thiserror", ] diff --git a/crypto/ethrex-crypto/src/lib.rs b/crypto/ethrex-crypto/src/lib.rs index c1e5d8446..6d625503d 100644 --- a/crypto/ethrex-crypto/src/lib.rs +++ b/crypto/ethrex-crypto/src/lib.rs @@ -16,11 +16,17 @@ //! Every other `Crypto` method inherits the trait default (vetted pure-Rust //! crates: `ark-bn254`, `bls12_381`, `p256`, `sha2`, `ripemd`, …). +// Only the host `keccak256` falls back to software keccak; the guest routes +// through the precompile, so on riscv64 this import would be unused. +#[cfg(not(target_arch = "riscv64"))] use ethrex_crypto::keccak::keccak_hash; use ethrex_crypto::{Crypto, CryptoError}; use k256::elliptic_curve::group::prime::PrimeCurveAffine; -use k256::elliptic_curve::ops::{Invert, LinearCombination, Reduce}; -use k256::elliptic_curve::point::DecompressPoint; +use k256::elliptic_curve::ops::{LinearCombination, Reduce}; +// `Invert` (software `x.invert()`) is only used by the host fallback; on the +// riscv64 guest all inversions go through the `hint` ecall. +#[cfg(not(target_arch = "riscv64"))] +use k256::elliptic_curve::ops::Invert; use k256::elliptic_curve::sec1::ToEncodedPoint; use k256::elliptic_curve::PrimeField; use k256::{AffinePoint, FieldBytes, ProjectivePoint, Scalar, U256}; @@ -77,6 +83,83 @@ impl Crypto for LambdaVmEcsmCrypto { /// We compute the recovery directly rather than calling k256's /// `recover_from_prehash`, which internally runs a *second* lincomb to /// re-verify the key — doubling the ECSM ecalls for no gain here. +/// Obtain a 32-byte big-endian hint for `x_be` via the executor `hint` ecall +/// (the host computes the modular inverse / sqrt; the value is provable via the +/// prover's HINT table). The result is UNVERIFIED — every caller MUST check it +/// in-guest (`x·inv == 1`, `y² == x³+7`), since the ecall adds no correctness +/// constraint. BENCH scaffolding. +#[cfg(target_arch = "riscv64")] +fn get_hint(hint_id: usize, x_be: &[u8; 32]) -> [u8; 32] { + let mut out = [0u8; 32]; + lambda_vm_syscalls::syscalls::hint(hint_id, &mut out, x_be); + out +} + +/// Scalar-field inverse `x⁻¹ mod n`. On riscv64 (guest) the inverse comes from the +/// `hint` ecall and we verify `x·inv == 1`; off-target (host tests) it computes the +/// inverse in software. BENCH scaffolding. +fn scalar_inv(x: &Scalar) -> Option { + #[cfg(target_arch = "riscv64")] + { + use k256::elliptic_curve::subtle::ConstantTimeEq; + let x_be: [u8; 32] = x.to_bytes().into(); + let inv_be = get_hint(lambda_vm_syscalls::syscalls::HINT_SCALAR_INV, &x_be); + let inv: Scalar = Option::from(Scalar::from_repr(inv_be.into()))?; + // Verify the untrusted hint: x·inv must equal 1 (mod n). + if bool::from((*x * inv).ct_eq(&Scalar::ONE)) { + Some(inv) + } else { + None + } + } + #[cfg(not(target_arch = "riscv64"))] + { + x.invert_vartime().into() + } +} + +/// Decompress R from its x-coordinate + parity. On riscv64 the `y = sqrt(x³+7)` +/// is an `hint`-ecall value verified in-guest (`y² == x³+7`), with parity +/// selection; off-target it uses k256's software `decompress`. BENCH scaffolding. +fn decompress_r(r_bytes: &FieldBytes, y_is_odd: bool) -> Option { + #[cfg(target_arch = "riscv64")] + { + let x: FieldElement = Option::from(FieldElement::from_bytes(r_bytes))?; + // secp256k1: y² = x³ + 7. + let mut seven_bytes = [0u8; 32]; + seven_bytes[31] = 7; + let seven: FieldElement = Option::from(FieldElement::from_bytes(&seven_bytes.into()))?; + let x3: FieldElement = x.square() * x; + let rhs: FieldElement = x3 + seven; + // Hinted sqrt (BE in/out), then verify y² == rhs canonically. + let rhs_be: [u8; 32] = rhs.to_bytes().into(); + let y_be = get_hint(lambda_vm_syscalls::syscalls::HINT_FIELD_SQRT, &rhs_be); + let mut y: FieldElement = Option::from(FieldElement::from_bytes(&y_be.into()))?; + let y2: FieldElement = y.square(); + // Verify the untrusted root: y² must equal x³+7. Negate `y2`, not `rhs`: + // `Neg` is `negate(1)` and only accepts magnitude 1, which `square()` always + // returns, whereas `rhs` is a sum and carries magnitude 2 — negating it would + // silently compute the wrong value in release, where the debug assert is gone. + // (`ct_eq` is unusable here for the same reason as in `field_inv`.) + if !bool::from((rhs + y2.negate(1)).normalizes_to_zero()) { + return None; + } + // Select the root whose canonical LSB matches the requested parity. + let y_odd = (y.to_bytes()[31] & 1) == 1; + if y_odd != y_is_odd { + y = -y; + } + // Build the affine point; `from_encoded_point` re-checks it's on-curve. + let ep = EncodedPoint::from_affine_coordinates(&x.to_bytes(), &y.to_bytes(), false); + Option::from(AffinePoint::from_encoded_point(&ep)) + } + #[cfg(not(target_arch = "riscv64"))] + { + use k256::elliptic_curve::point::DecompressPoint; + AffinePoint::decompress(r_bytes, u8::from(y_is_odd).into()).into() + } +} + fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 64], CryptoError> { let r_bytes = <&FieldBytes>::from(&sig[..32]); let s_bytes = <&FieldBytes>::from(&sig[32..]); @@ -96,15 +179,14 @@ fn ecsm_ecrecover(sig: &[u8; 64], recid: u8, msg: &[u8; 32]) -> Result<[u8; 64], // precompile; we don't handle it (decompression simply fails), matching the // trait default. let y_is_odd = (recid & 1) != 0; - let r_point: Option = - AffinePoint::decompress(r_bytes, u8::from(y_is_odd).into()).into(); + let r_point: Option = decompress_r(r_bytes, y_is_odd); let Some(r_point) = r_point else { return Err(CryptoError::RecoveryFailed); }; let r_proj = ProjectivePoint::from(r_point); let z = >::reduce_bytes(&FieldBytes::from(*msg)); - let r_inv: Option = r.invert_vartime().into(); + let r_inv: Option = scalar_inv(&r); let Some(r_inv) = r_inv else { return Err(CryptoError::RecoveryFailed); }; @@ -194,6 +276,34 @@ fn ecsm_oracle(x: &FieldElement, k: &Scalar) -> Option { /// then `Q = A + B` is one affine addition. All three inversions are batched. /// /// Generic over the oracle so unit tests can substitute a software stand-in. +/// Base-field inverse `x⁻¹ mod p`. On riscv64 the host supplies it via the +/// `hint` ecall and we verify `x·inv == 1`; off-target it inverts in software. +#[cfg(any(target_arch = "riscv64", test))] +fn field_inv(x: &FieldElement) -> Option { + #[cfg(target_arch = "riscv64")] + { + let x_be: [u8; 32] = x.to_bytes().into(); + let inv_be = get_hint(lambda_vm_syscalls::syscalls::HINT_FIELD_INV, &x_be); + let inv: FieldElement = Option::from(FieldElement::from_bytes(&inv_be.into()))?; + // Verify the untrusted hint: x·inv must equal 1 (mod p). Compare by asking + // whether the difference normalizes to zero — a value-level test that skips + // the two full normalizations a `to_bytes()` compare pays. `ct_eq` is NOT a + // substitute: k256's FieldElement compares raw limbs *and* the magnitude and + // `normalized` tags, so a `mul` result (magnitude 1, unnormalized) never + // compares equal to the normalized `ONE` constant whatever its value. + // `Neg` is `negate(1)`, valid here because `mul` yields magnitude 1. + if bool::from((*x * inv - FieldElement::ONE).normalizes_to_zero()) { + Some(inv) + } else { + None + } + } + #[cfg(not(target_arch = "riscv64"))] + { + Option::from(x.invert()) + } +} + #[cfg(any(target_arch = "riscv64", test))] fn lincomb2_with_oracle( a1: &AffinePoint, @@ -232,7 +342,7 @@ where // One shared inversion for the two λ denominators and the final chord. let den1 = y1.double() * dx1; let den2 = y2.double() * dx2; - let inv = Option::::from((den1 * den2 * dxq).invert())?; + let inv = field_inv(&(den1 * den2 * dxq))?; let inv_den1 = inv * den2 * dxq; let inv_den2 = inv * den1 * dxq; let inv_dxq = inv * den1 * den2; diff --git a/crypto/tiny-keccak/Cargo.toml b/crypto/tiny-keccak/Cargo.toml new file mode 100644 index 000000000..59bb13d1b --- /dev/null +++ b/crypto/tiny-keccak/Cargo.toml @@ -0,0 +1,38 @@ +# Vendored fork of tiny-keccak 2.0.2 (CC0-1.0), used ONLY as a `[patch.crates-io]` +# override in the ethrex guest so every Keccak permutation (Ethereum trie/RLP hashing +# via ethrex-crypto's `keccak_hash` + `ethbloom`) routes through the lambda_vm +# `keccak_permute` accelerator syscall instead of the software ARX permutation. +# +# The ONLY change vs upstream 2.0.2 is `src/keccakf.rs`: on riscv64 the `keccakf` +# permutation calls `lambda_vm_syscalls::syscalls::keccak_permute`; on every other +# target it runs the original software permutation (so host builds/tests are unchanged). +# Keep name/version = tiny-keccak/2.0.2 so the crates.io patch resolves. +[package] +name = "tiny-keccak" +version = "2.0.2" +authors = ["debris "] +description = "An implementation of Keccak derived functions (lambda_vm keccak-syscall fork)." +edition = "2018" +license = "CC0-1.0" +homepage = "https://github.com/debris/tiny-keccak" + +[dependencies] +crunchy = "0.2.2" + +# The keccak-permute accelerator syscall only exists on the riscv64 guest; on host +# the software permutation is used, so this dependency is guest-only. +[target.'cfg(target_arch = "riscv64")'.dependencies] +lambda-vm-syscalls = { path = "../../syscalls" } + +[features] +cshake = [] +default = [] +fips202 = ["keccak", "shake", "sha3"] +k12 = [] +keccak = [] +kmac = ["cshake"] +parallel_hash = ["cshake"] +sha3 = [] +shake = [] +sp800 = ["cshake", "kmac", "tuple_hash"] +tuple_hash = ["cshake"] diff --git a/crypto/tiny-keccak/LICENSE b/crypto/tiny-keccak/LICENSE new file mode 100644 index 000000000..0e259d42c --- /dev/null +++ b/crypto/tiny-keccak/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/crypto/tiny-keccak/src/cshake.rs b/crypto/tiny-keccak/src/cshake.rs new file mode 100644 index 000000000..d635e5b6b --- /dev/null +++ b/crypto/tiny-keccak/src/cshake.rs @@ -0,0 +1,77 @@ +//! The `cSHAKE` extendable-output functions defined in [`SP800-185`]. +//! +//! [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf + +use crate::{bits_to_rate, keccakf::KeccakF, left_encode, Hasher, KeccakState, Xof}; + +/// The `cSHAKE` extendable-output functions defined in [`SP800-185`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["cshake"] } +/// ``` +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +#[derive(Clone)] +pub struct CShake { + state: KeccakState, +} + +impl CShake { + const DELIM: u8 = 0x04; + + /// Creates new [`CShake`] hasher with a security level of 128 bits. + /// + /// [`CShake`]: struct.CShake.html + pub fn v128(name: &[u8], custom_string: &[u8]) -> CShake { + CShake::new(name, custom_string, 128) + } + + /// Creates new [`CShake`] hasher with a security level of 256 bits. + /// + /// [`CShake`]: struct.CShake.html + pub fn v256(name: &[u8], custom_string: &[u8]) -> CShake { + CShake::new(name, custom_string, 256) + } + + pub(crate) fn new(name: &[u8], custom_string: &[u8], bits: usize) -> CShake { + let rate = bits_to_rate(bits); + // if there is no name and no customization string + // cSHAKE is SHAKE + if name.is_empty() && custom_string.is_empty() { + let state = KeccakState::new(rate, 0x1f); + return CShake { state }; + } + + let mut state = KeccakState::new(rate, Self::DELIM); + state.update(left_encode(rate).value()); + state.update(left_encode(name.len() * 8).value()); + state.update(name); + state.update(left_encode(custom_string.len() * 8).value()); + state.update(custom_string); + state.fill_block(); + CShake { state } + } + + pub(crate) fn fill_block(&mut self) { + self.state.fill_block(); + } +} + +impl Hasher for CShake { + fn update(&mut self, input: &[u8]) { + self.state.update(input); + } + + fn finalize(self, output: &mut [u8]) { + self.state.finalize(output); + } +} + +impl Xof for CShake { + fn squeeze(&mut self, output: &mut [u8]) { + self.state.squeeze(output); + } +} diff --git a/crypto/tiny-keccak/src/k12.rs b/crypto/tiny-keccak/src/k12.rs new file mode 100644 index 000000000..a205d3d08 --- /dev/null +++ b/crypto/tiny-keccak/src/k12.rs @@ -0,0 +1,160 @@ +//! The `KangarooTwelve` hash function defined [`here`]. +//! +//! [`here`]: https://eprint.iacr.org/2016/770.pdf + +use crate::{bits_to_rate, keccakp::KeccakP, EncodedLen, Hasher, IntoXof, KeccakState, Xof}; + +fn encode_len(len: usize) -> EncodedLen { + let len_view = (len as u64).to_be_bytes(); + let offset = len_view.iter().position(|i| *i != 0).unwrap_or(8); + let mut buffer = [0u8; 9]; + buffer[..8].copy_from_slice(&len_view); + buffer[8] = 8 - offset as u8; + + EncodedLen { offset, buffer } +} + +/// The `KangarooTwelve` hash function defined [`here`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["k12"] } +/// ``` +/// +/// [`here`]: https://eprint.iacr.org/2016/770.pdf +#[derive(Clone)] +pub struct KangarooTwelve { + state: KeccakState, + current_chunk: KeccakState, + custom_string: Option, + written: usize, + chunks: usize, +} + +impl KangarooTwelve { + const MAX_CHUNK_SIZE: usize = 8192; + + /// Creates new [`KangarooTwelve`] hasher with a security level of 128 bits. + /// + /// [`KangarooTwelve`]: struct.KangarooTwelve.html + pub fn new(custom_string: T) -> Self { + let rate = bits_to_rate(128); + KangarooTwelve { + state: KeccakState::new(rate, 0), + current_chunk: KeccakState::new(rate, 0x0b), + custom_string: Some(custom_string), + written: 0, + chunks: 0, + } + } +} + +impl> Hasher for KangarooTwelve { + fn update(&mut self, input: &[u8]) { + let mut to_absorb = input; + if self.chunks == 0 { + let todo = core::cmp::min(Self::MAX_CHUNK_SIZE - self.written, to_absorb.len()); + self.state.update(&to_absorb[..todo]); + self.written += todo; + to_absorb = &to_absorb[todo..]; + + if to_absorb.len() > 0 && self.written == Self::MAX_CHUNK_SIZE { + self.state.update(&[0x03, 0, 0, 0, 0, 0, 0, 0]); + self.written = 0; + self.chunks += 1; + } + } + + while to_absorb.len() > 0 { + if self.written == Self::MAX_CHUNK_SIZE { + let mut chunk_hash = [0u8; 32]; + let current_chunk = self.current_chunk.clone(); + self.current_chunk.reset(); + current_chunk.finalize(&mut chunk_hash); + self.state.update(&chunk_hash); + self.written = 0; + self.chunks += 1; + } + + let todo = core::cmp::min(Self::MAX_CHUNK_SIZE - self.written, to_absorb.len()); + self.current_chunk.update(&to_absorb[..todo]); + self.written += todo; + to_absorb = &to_absorb[todo..]; + } + } + + fn finalize(self, output: &mut [u8]) { + let mut xof = self.into_xof(); + xof.squeeze(output); + } +} + +/// The `KangarooTwelve` extendable-output function defined [`here`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["k12"] } +/// ``` +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::{KangarooTwelve, Xof, IntoXof, Hasher}; +/// let input = b"hello world"; +/// let mut output = [0u8; 64]; +/// let mut hasher = KangarooTwelve::new(b""); +/// hasher.update(input); +/// let mut xof = hasher.into_xof(); +/// xof.squeeze(&mut output[..32]); +/// xof.squeeze(&mut output[32..]); +/// ``` +/// +/// --- +/// +/// [`KangarooTwelveXof`] can be created only by using [`KangarooTwelve::IntoXof`] interface. +/// +/// [`here`]: https://eprint.iacr.org/2016/770.pdf +/// [`KangarooTwelveXof`]: struct.KangarooTwelveXof.html +/// [`KangarooTwelve::IntoXof`]: struct.KangarooTwelve.html#impl-IntoXof +#[derive(Clone)] +pub struct KangarooTwelveXof { + state: KeccakState, +} + +impl> IntoXof for KangarooTwelve { + type Xof = KangarooTwelveXof; + + fn into_xof(mut self) -> KangarooTwelveXof { + let custom_string = self + .custom_string + .take() + .expect("KangarooTwelve cannot be initialized without custom_string; qed"); + let encoded_len = encode_len(custom_string.as_ref().len()); + self.update(custom_string.as_ref()); + self.update(encoded_len.value()); + + if self.chunks == 0 { + self.state.delim = 0x07; + } else { + let encoded_chunks = encode_len(self.chunks); + let mut tmp_chunk = [0u8; 32]; + self.current_chunk.finalize(&mut tmp_chunk); + self.state.update(&tmp_chunk); + self.state.update(encoded_chunks.value()); + self.state.update(&[0xff, 0xff]); + self.state.delim = 0x06; + } + + KangarooTwelveXof { state: self.state } + } +} + +impl Xof for KangarooTwelveXof { + fn squeeze(&mut self, output: &mut [u8]) { + self.state.squeeze(output); + } +} diff --git a/crypto/tiny-keccak/src/keccak.rs b/crypto/tiny-keccak/src/keccak.rs new file mode 100644 index 000000000..a7db0b835 --- /dev/null +++ b/crypto/tiny-keccak/src/keccak.rs @@ -0,0 +1,93 @@ +//! The `Keccak` hash functions. + +use super::{bits_to_rate, keccakf::KeccakF, Hasher, KeccakState}; + +/// The `Keccak` hash functions defined in [`Keccak SHA3 submission`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["keccak"] } +/// ``` +/// +/// [`Keccak SHA3 submission`]: https://keccak.team/files/Keccak-submission-3.pdf +#[derive(Clone)] +pub struct Keccak { + state: KeccakState, +} + +impl Keccak { + const DELIM: u8 = 0x01; + + /// Creates new [`Keccak`] hasher with a security level of 224 bits. + /// + /// [`Keccak`]: struct.Keccak.html + pub fn v224() -> Keccak { + Keccak::new(224) + } + + /// Creates new [`Keccak`] hasher with a security level of 256 bits. + /// + /// [`Keccak`]: struct.Keccak.html + pub fn v256() -> Keccak { + Keccak::new(256) + } + + /// Creates new [`Keccak`] hasher with a security level of 384 bits. + /// + /// [`Keccak`]: struct.Keccak.html + pub fn v384() -> Keccak { + Keccak::new(384) + } + + /// Creates new [`Keccak`] hasher with a security level of 512 bits. + /// + /// [`Keccak`]: struct.Keccak.html + pub fn v512() -> Keccak { + Keccak::new(512) + } + + fn new(bits: usize) -> Keccak { + Keccak { + state: KeccakState::new(bits_to_rate(bits), Self::DELIM), + } + } +} + +impl Hasher for Keccak { + /// Absorb additional input. Can be called multiple times. + /// + /// # Example + /// + /// ``` + /// # use tiny_keccak::{Hasher, Keccak}; + /// # + /// # fn main() { + /// # let mut keccak = Keccak::v256(); + /// keccak.update(b"hello"); + /// keccak.update(b" world"); + /// # } + /// ``` + fn update(&mut self, input: &[u8]) { + self.state.update(input); + } + + /// Pad and squeeze the state to the output. + /// + /// # Example + /// + /// ``` + /// # use tiny_keccak::{Hasher, Keccak}; + /// # + /// # fn main() { + /// # let keccak = Keccak::v256(); + /// # let mut output = [0u8; 32]; + /// keccak.finalize(&mut output); + /// # } + /// # + /// ``` + fn finalize(self, output: &mut [u8]) { + self.state.finalize(output); + } +} diff --git a/crypto/tiny-keccak/src/keccakf.rs b/crypto/tiny-keccak/src/keccakf.rs new file mode 100644 index 000000000..aceeec614 --- /dev/null +++ b/crypto/tiny-keccak/src/keccakf.rs @@ -0,0 +1,67 @@ +use crate::{Buffer, Permutation}; + +// The software permutation and its round constants are only used off the riscv64 +// guest (where `keccakf` dispatches to the accelerator syscall); gate them so the +// guest build has no dead code. +#[cfg(not(target_arch = "riscv64"))] +const ROUNDS: usize = 24; + +#[cfg(not(target_arch = "riscv64"))] +const RC: [u64; ROUNDS] = [ + 1u64, + 0x8082u64, + 0x800000000000808au64, + 0x8000000080008000u64, + 0x808bu64, + 0x80000001u64, + 0x8000000080008081u64, + 0x8000000000008009u64, + 0x8au64, + 0x88u64, + 0x80008009u64, + 0x8000000au64, + 0x8000808bu64, + 0x800000000000008bu64, + 0x8000000000008089u64, + 0x8000000000008003u64, + 0x8000000000008002u64, + 0x8000000000000080u64, + 0x800au64, + 0x800000008000000au64, + 0x8000000080008081u64, + 0x8000000000008080u64, + 0x80000001u64, + 0x8000000080008008u64, +]; + +// Original software permutation, kept as `keccakf_software`. On host targets the +// public `keccakf` is exactly this; on the riscv64 guest it is replaced by the +// accelerator syscall below (so the software permutation is compiled out entirely). +#[cfg(not(target_arch = "riscv64"))] +keccak_function!("`keccak-f[1600, 24]`", keccakf_software, ROUNDS, RC); + +/// keccak-f[1600, 24]. +/// +/// LAMBDA_VM FORK: on the riscv64 guest this dispatches to the `keccak_permute` +/// accelerator syscall (one ecall + 25 word reads/writes proven by the KECCAK +/// table, instead of ~600 ARX instructions on the CPU table); on every other +/// target it runs the unmodified software permutation. The state layout is the +/// canonical `[u64; 25]` lane order, identical on both sides — verified end to +/// end by the ethrex trie-root differential tests. +#[cfg(target_arch = "riscv64")] +pub fn keccakf(a: &mut [u64; crate::WORDS]) { + lambda_vm_syscalls::syscalls::keccak_permute(a); +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn keccakf(a: &mut [u64; crate::WORDS]) { + keccakf_software(a); +} + +pub struct KeccakF; + +impl Permutation for KeccakF { + fn execute(buffer: &mut Buffer) { + keccakf(buffer.words()); + } +} diff --git a/crypto/tiny-keccak/src/keccakp.rs b/crypto/tiny-keccak/src/keccakp.rs new file mode 100644 index 000000000..f747d4922 --- /dev/null +++ b/crypto/tiny-keccak/src/keccakp.rs @@ -0,0 +1,28 @@ +use crate::{Buffer, Permutation}; + +const ROUNDS: usize = 12; + +const RC: [u64; ROUNDS] = [ + 0x000000008000808b, + 0x800000000000008b, + 0x8000000000008089, + 0x8000000000008003, + 0x8000000000008002, + 0x8000000000000080, + 0x000000000000800a, + 0x800000008000000a, + 0x8000000080008081, + 0x8000000000008080, + 0x0000000080000001, + 0x8000000080008008, +]; + +keccak_function!("`keccak-p[1600, 12]`", keccakp, ROUNDS, RC); + +pub struct KeccakP; + +impl Permutation for KeccakP { + fn execute(buffer: &mut Buffer) { + keccakp(buffer.words()); + } +} diff --git a/crypto/tiny-keccak/src/kmac.rs b/crypto/tiny-keccak/src/kmac.rs new file mode 100644 index 000000000..d3741c793 --- /dev/null +++ b/crypto/tiny-keccak/src/kmac.rs @@ -0,0 +1,114 @@ +use crate::{bits_to_rate, left_encode, right_encode, CShake, Hasher, IntoXof, Xof}; + +/// The `KMAC` pseudo-random functions defined in [`SP800-185`]. +/// +/// The KECCAK Message Authentication Code (`KMAC`) algorithm is a `PRF` and keyed hash function based +/// on KECCAK. It provides variable-length output, and unlike [`SHAKE`] and [`cSHAKE`], altering the +/// requested output length generates a new, unrelated output. KMAC has two variants, [`KMAC128`] and +/// [`KMAC256`], built from [`cSHAKE128`] and [`cSHAKE256`], respectively. The two variants differ somewhat in +/// their technical security properties. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["kmac"] } +/// ``` +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +/// [`KMAC128`]: struct.Kmac.html#method.v128 +/// [`KMAC256`]: struct.Kmac.html#method.v256 +/// [`SHAKE`]: struct.Shake.html +/// [`cSHAKE`]: struct.CShake.html +/// [`cSHAKE128`]: struct.CShake.html#method.v128 +/// [`cSHAKE256`]: struct.CShake.html#method.v256 +#[derive(Clone)] +pub struct Kmac { + state: CShake, +} + +impl Kmac { + /// Creates new [`Kmac`] hasher with a security level of 128 bits. + /// + /// [`Kmac`]: struct.Kmac.html + pub fn v128(key: &[u8], custom_string: &[u8]) -> Kmac { + Kmac::new(key, custom_string, 128) + } + + /// Creates new [`Kmac`] hasher with a security level of 256 bits. + /// + /// [`Kmac`]: struct.Kmac.html + pub fn v256(key: &[u8], custom_string: &[u8]) -> Kmac { + Kmac::new(key, custom_string, 256) + } + + fn new(key: &[u8], custom_string: &[u8], bits: usize) -> Kmac { + let rate = bits_to_rate(bits); + let mut state = CShake::new(b"KMAC", custom_string, bits); + state.update(left_encode(rate).value()); + state.update(left_encode(key.len() * 8).value()); + state.update(key); + state.fill_block(); + Kmac { state } + } +} + +impl Hasher for Kmac { + fn update(&mut self, input: &[u8]) { + self.state.update(input) + } + + fn finalize(mut self, output: &mut [u8]) { + self.state.update(right_encode(output.len() * 8).value()); + self.state.finalize(output) + } +} + +/// The `KMACXOF` extendable-output functions defined in [`SP800-185`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["kmac"] } +/// ``` +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::{Kmac, Xof, IntoXof, Hasher}; +/// let input = b"hello world"; +/// let mut output = [0u8; 64]; +/// let mut kmac = Kmac::v256(b"", b""); +/// kmac.update(input); +/// let mut xof = kmac.into_xof(); +/// xof.squeeze(&mut output[..32]); +/// xof.squeeze(&mut output[32..]); +/// ``` +/// +/// --- +/// +/// [`KmacXof`] can be created only by using [`Kmac::IntoXof`] interface. +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +/// [`KmacXof`]: struct.KmacXof.html +/// [`Kmac::IntoXof`]: struct.Kmac.html#impl-IntoXof +#[derive(Clone)] +pub struct KmacXof { + state: CShake, +} + +impl IntoXof for Kmac { + type Xof = KmacXof; + + fn into_xof(mut self) -> Self::Xof { + self.state.update(right_encode(0).value()); + KmacXof { state: self.state } + } +} + +impl Xof for KmacXof { + fn squeeze(&mut self, output: &mut [u8]) { + self.state.squeeze(output) + } +} diff --git a/crypto/tiny-keccak/src/lib.rs b/crypto/tiny-keccak/src/lib.rs new file mode 100644 index 000000000..9329fd37a --- /dev/null +++ b/crypto/tiny-keccak/src/lib.rs @@ -0,0 +1,501 @@ +//! Keccak derived functions specified in [`FIPS-202`], [`SP800-185`] and [`KangarooTwelve`]. +//! +//! # Example +//! +//! ``` +//! # use tiny_keccak::Hasher; +//! # +//! # fn foo(mut hasher: H) { +//! let input_a = b"hello world"; +//! let input_b = b"!"; +//! let mut output = [0u8; 32]; +//! hasher.update(input_a); +//! hasher.update(input_b); +//! hasher.finalize(&mut output); +//! # } +//! ``` +//! +//! # Credits +//! +//! - [`coruus/keccak-tiny`] for C implementation of keccak function +//! - [`@quininer`] for `no-std` support and rust implementation [`SP800-185`] +//! - [`mimoo/GoKangarooTwelve`] for GO implementation of `KangarooTwelve` +//! - [`@Vurich`] for optimizations +//! - [`@oleganza`] for adding support for half-duplex use +//! +//! # License +//! +//! [`CC0`]. Attribution kindly requested. Blame taken too, +//! but not liability. +//! +//! [`FIPS-202`]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf +//! [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +//! [`KangarooTwelve`]: https://eprint.iacr.org/2016/770.pdf +//! [`coruus/keccak-tiny`]: https://github.com/coruus/keccak-tiny +//! [`mimoo/GoKangarooTwelve`]: https://github.com/mimoo/GoKangarooTwelve +//! [`@quininer`]: https://github.com/quininer +//! [`@Vurich`]: https://github.com/Vurich +//! [`@oleganza`]: https://github.com/oleganza +//! [`CC0`]: https://github.com/debris/tiny-keccak/blob/master/LICENSE + +#![no_std] +#![deny(missing_docs)] + +const RHO: [u32; 24] = [ + 1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44, +]; + +const PI: [usize; 24] = [ + 10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1, +]; + +const WORDS: usize = 25; + +macro_rules! keccak_function { + ($doc: expr, $name: ident, $rounds: expr, $rc: expr) => { + #[doc = $doc] + #[allow(unused_assignments)] + #[allow(non_upper_case_globals)] + pub fn $name(a: &mut [u64; $crate::WORDS]) { + use crunchy::unroll; + + for i in 0..$rounds { + let mut array: [u64; 5] = [0; 5]; + + // Theta + unroll! { + for x in 0..5 { + unroll! { + for y_count in 0..5 { + let y = y_count * 5; + array[x] ^= a[x + y]; + } + } + } + } + + unroll! { + for x in 0..5 { + unroll! { + for y_count in 0..5 { + let y = y_count * 5; + a[y + x] ^= array[(x + 4) % 5] ^ array[(x + 1) % 5].rotate_left(1); + } + } + } + } + + // Rho and pi + let mut last = a[1]; + unroll! { + for x in 0..24 { + array[0] = a[$crate::PI[x]]; + a[$crate::PI[x]] = last.rotate_left($crate::RHO[x]); + last = array[0]; + } + } + + // Chi + unroll! { + for y_step in 0..5 { + let y = y_step * 5; + + unroll! { + for x in 0..5 { + array[x] = a[y + x]; + } + } + + unroll! { + for x in 0..5 { + a[y + x] = array[x] ^ ((!array[(x + 1) % 5]) & (array[(x + 2) % 5])); + } + } + } + }; + + // Iota + a[0] ^= $rc[i]; + } + } + } +} + +#[cfg(feature = "k12")] +mod keccakp; + +#[cfg(feature = "k12")] +pub use keccakp::keccakp; + +#[cfg(any( + feature = "keccak", + feature = "shake", + feature = "sha3", + feature = "cshake", + feature = "kmac", + feature = "tuple_hash", + feature = "parallel_hash" +))] +mod keccakf; + +#[cfg(any( + feature = "keccak", + feature = "shake", + feature = "sha3", + feature = "cshake", + feature = "kmac", + feature = "tuple_hash", + feature = "parallel_hash" +))] +pub use keccakf::keccakf; + +#[cfg(feature = "k12")] +mod k12; + +#[cfg(feature = "k12")] +pub use k12::{KangarooTwelve, KangarooTwelveXof}; + +#[cfg(feature = "keccak")] +mod keccak; + +#[cfg(feature = "keccak")] +pub use keccak::Keccak; + +#[cfg(feature = "shake")] +mod shake; + +#[cfg(feature = "shake")] +pub use shake::Shake; + +#[cfg(feature = "sha3")] +mod sha3; + +#[cfg(feature = "sha3")] +pub use sha3::Sha3; + +#[cfg(feature = "cshake")] +mod cshake; + +#[cfg(feature = "cshake")] +pub use cshake::CShake; + +#[cfg(feature = "kmac")] +mod kmac; + +#[cfg(feature = "kmac")] +pub use kmac::{Kmac, KmacXof}; + +#[cfg(feature = "tuple_hash")] +mod tuple_hash; + +#[cfg(feature = "tuple_hash")] +pub use tuple_hash::{TupleHash, TupleHashXof}; + +#[cfg(feature = "parallel_hash")] +mod parallel_hash; + +#[cfg(feature = "parallel_hash")] +pub use parallel_hash::{ParallelHash, ParallelHashXof}; + +/// A trait for hashing an arbitrary stream of bytes. +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::Hasher; +/// # +/// # fn foo(mut hasher: H) { +/// let input_a = b"hello world"; +/// let input_b = b"!"; +/// let mut output = [0u8; 32]; +/// hasher.update(input_a); +/// hasher.update(input_b); +/// hasher.finalize(&mut output); +/// # } +/// ``` +pub trait Hasher { + /// Absorb additional input. Can be called multiple times. + fn update(&mut self, input: &[u8]); + + /// Pad and squeeze the state to the output. + fn finalize(self, output: &mut [u8]); +} + +/// A trait used to convert [`Hasher`] into it's [`Xof`] counterpart. +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::IntoXof; +/// # +/// # fn foo(hasher: H) { +/// let xof = hasher.into_xof(); +/// # } +/// ``` +/// +/// [`Hasher`]: trait.Hasher.html +/// [`Xof`]: trait.Xof.html +pub trait IntoXof { + /// A type implementing [`Xof`], eXtendable-output function interface. + /// + /// [`Xof`]: trait.Xof.html + type Xof: Xof; + + /// A method used to convert type into [`Xof`]. + /// + /// [`Xof`]: trait.Xof.html + fn into_xof(self) -> Self::Xof; +} + +/// Extendable-output function (`XOF`) is a function on bit strings in which the output can be +/// extended to any desired length. +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::Xof; +/// # +/// # fn foo(mut xof: X) { +/// let mut output = [0u8; 64]; +/// xof.squeeze(&mut output[0..32]); +/// xof.squeeze(&mut output[32..]); +/// # } +/// ``` +pub trait Xof { + /// A method used to retrieve another part of hash function output. + fn squeeze(&mut self, output: &mut [u8]); +} + +struct EncodedLen { + offset: usize, + buffer: [u8; 9], +} + +impl EncodedLen { + fn value(&self) -> &[u8] { + &self.buffer[self.offset..] + } +} + +fn left_encode(len: usize) -> EncodedLen { + let mut buffer = [0u8; 9]; + buffer[1..].copy_from_slice(&(len as u64).to_be_bytes()); + let offset = buffer.iter().position(|i| *i != 0).unwrap_or(8); + buffer[offset - 1] = 9 - offset as u8; + + EncodedLen { + offset: offset - 1, + buffer, + } +} + +fn right_encode(len: usize) -> EncodedLen { + let mut buffer = [0u8; 9]; + buffer[..8].copy_from_slice(&(len as u64).to_be_bytes()); + let offset = buffer.iter().position(|i| *i != 0).unwrap_or(7); + buffer[8] = 8 - offset as u8; + EncodedLen { offset, buffer } +} + +#[derive(Default, Clone)] +struct Buffer([u64; WORDS]); + +impl Buffer { + fn words(&mut self) -> &mut [u64; WORDS] { + &mut self.0 + } + + #[cfg(target_endian = "little")] + #[inline] + fn execute(&mut self, offset: usize, len: usize, f: F) { + let buffer: &mut [u8; WORDS * 8] = unsafe { core::mem::transmute(&mut self.0) }; + f(&mut buffer[offset..][..len]); + } + + #[cfg(target_endian = "big")] + #[inline] + fn execute(&mut self, offset: usize, len: usize, f: F) { + fn swap_endianess(buffer: &mut [u64]) { + for item in buffer { + *item = item.swap_bytes(); + } + } + + let start = offset / 8; + let end = (offset + len + 7) / 8; + swap_endianess(&mut self.0[start..end]); + let buffer: &mut [u8; WORDS * 8] = unsafe { core::mem::transmute(&mut self.0) }; + f(&mut buffer[offset..][..len]); + swap_endianess(&mut self.0[start..end]); + } + + fn setout(&mut self, dst: &mut [u8], offset: usize, len: usize) { + self.execute(offset, len, |buffer| dst[..len].copy_from_slice(buffer)); + } + + fn xorin(&mut self, src: &[u8], offset: usize, len: usize) { + self.execute(offset, len, |dst| { + assert!(dst.len() <= src.len()); + let len = dst.len(); + let mut dst_ptr = dst.as_mut_ptr(); + let mut src_ptr = src.as_ptr(); + for _ in 0..len { + unsafe { + *dst_ptr ^= *src_ptr; + src_ptr = src_ptr.offset(1); + dst_ptr = dst_ptr.offset(1); + } + } + }); + } + + fn pad(&mut self, offset: usize, delim: u8, rate: usize) { + self.execute(offset, 1, |buff| buff[0] ^= delim); + self.execute(rate - 1, 1, |buff| buff[0] ^= 0x80); + } +} + +trait Permutation { + fn execute(a: &mut Buffer); +} + +#[derive(Clone, Copy)] +enum Mode { + Absorbing, + Squeezing, +} + +struct KeccakState

{ + buffer: Buffer, + offset: usize, + rate: usize, + delim: u8, + mode: Mode, + permutation: core::marker::PhantomData

, +} + +impl

Clone for KeccakState

{ + fn clone(&self) -> Self { + KeccakState { + buffer: self.buffer.clone(), + offset: self.offset, + rate: self.rate, + delim: self.delim, + mode: self.mode, + permutation: core::marker::PhantomData, + } + } +} + +impl KeccakState

{ + fn new(rate: usize, delim: u8) -> Self { + assert!(rate != 0, "rate cannot be equal 0"); + KeccakState { + buffer: Buffer::default(), + offset: 0, + rate, + delim, + mode: Mode::Absorbing, + permutation: core::marker::PhantomData, + } + } + + fn keccak(&mut self) { + P::execute(&mut self.buffer); + } + + fn update(&mut self, input: &[u8]) { + if let Mode::Squeezing = self.mode { + self.mode = Mode::Absorbing; + self.fill_block(); + } + + //first foldp + let mut ip = 0; + let mut l = input.len(); + let mut rate = self.rate - self.offset; + let mut offset = self.offset; + while l >= rate { + self.buffer.xorin(&input[ip..], offset, rate); + self.keccak(); + ip += rate; + l -= rate; + rate = self.rate; + offset = 0; + } + + self.buffer.xorin(&input[ip..], offset, l); + self.offset = offset + l; + } + + fn pad(&mut self) { + self.buffer.pad(self.offset, self.delim, self.rate); + } + + fn squeeze(&mut self, output: &mut [u8]) { + if let Mode::Absorbing = self.mode { + self.mode = Mode::Squeezing; + self.pad(); + self.fill_block(); + } + + // second foldp + let mut op = 0; + let mut l = output.len(); + let mut rate = self.rate - self.offset; + let mut offset = self.offset; + while l >= rate { + self.buffer.setout(&mut output[op..], offset, rate); + self.keccak(); + op += rate; + l -= rate; + rate = self.rate; + offset = 0; + } + + self.buffer.setout(&mut output[op..], offset, l); + self.offset = offset + l; + } + + fn finalize(mut self, output: &mut [u8]) { + self.squeeze(output); + } + + fn fill_block(&mut self) { + self.keccak(); + self.offset = 0; + } + + fn reset(&mut self) { + self.buffer = Buffer::default(); + self.offset = 0; + self.mode = Mode::Absorbing; + } +} + +fn bits_to_rate(bits: usize) -> usize { + 200 - bits / 4 +} + +#[cfg(test)] +mod tests { + use crate::{left_encode, right_encode}; + + #[test] + fn test_left_encode() { + assert_eq!(left_encode(0).value(), &[1, 0]); + assert_eq!(left_encode(128).value(), &[1, 128]); + assert_eq!(left_encode(65536).value(), &[3, 1, 0, 0]); + assert_eq!(left_encode(4096).value(), &[2, 16, 0]); + assert_eq!(left_encode(54321).value(), &[2, 212, 49]); + } + + #[test] + fn test_right_encode() { + assert_eq!(right_encode(0).value(), &[0, 1]); + assert_eq!(right_encode(128).value(), &[128, 1]); + assert_eq!(right_encode(65536).value(), &[1, 0, 0, 3]); + assert_eq!(right_encode(4096).value(), &[16, 0, 2]); + assert_eq!(right_encode(54321).value(), &[212, 49, 2]); + } +} diff --git a/crypto/tiny-keccak/src/parallel_hash.rs b/crypto/tiny-keccak/src/parallel_hash.rs new file mode 100644 index 000000000..cc4581862 --- /dev/null +++ b/crypto/tiny-keccak/src/parallel_hash.rs @@ -0,0 +1,206 @@ +use crate::{left_encode, right_encode, CShake, Hasher, IntoXof, Xof}; + +#[derive(Clone)] +struct UnfinishedState { + state: CShake, + absorbed: usize, +} + +struct Suboutout { + state: [u8; 64], + size: usize, +} + +impl Suboutout { + fn security(bits: usize) -> Suboutout { + Suboutout { + state: [0u8; 64], + // 128 => 32, 256 => 64 + size: bits / 4, + } + } + + #[inline] + fn as_bytes(&self) -> &[u8] { + &self.state[..self.size] + } + + #[inline] + fn as_bytes_mut(&mut self) -> &mut [u8] { + &mut self.state[..self.size] + } +} + +/// The `ParallelHash` hash functions defined in [`SP800-185`]. +/// +/// The purpose of `ParallelHash` is to support the efficient hashing of very long strings, by +/// taking advantage of the parallelism available in modern processors. `ParallelHash` supports the +/// [`128-bit`] and [`256-bit`] security strengths, and also provides variable-length output. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["parallel_hash"] } +/// ``` +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +/// [`128-bit`]: struct.ParallelHash.html#method.v128 +/// [`256-bit`]: struct.ParallelHash.html#method.v256 +#[derive(Clone)] +pub struct ParallelHash { + state: CShake, + block_size: usize, + bits: usize, + blocks: usize, + unfinished: Option, +} + +impl ParallelHash { + /// Creates new [`ParallelHash`] hasher with a security level of 128 bits. + /// + /// [`ParallelHash`]: struct.ParallelHash.html + pub fn v128(custom_string: &[u8], block_size: usize) -> ParallelHash { + ParallelHash::new(custom_string, block_size, 128) + } + + /// Creates new [`ParallelHash`] hasher with a security level of 256 bits. + /// + /// [`ParallelHash`]: struct.ParallelHash.html + pub fn v256(custom_string: &[u8], block_size: usize) -> ParallelHash { + ParallelHash::new(custom_string, block_size, 256) + } + + fn new(custom_string: &[u8], block_size: usize, bits: usize) -> ParallelHash { + let mut state = CShake::new(b"ParallelHash", custom_string, bits); + state.update(left_encode(block_size).value()); + ParallelHash { + state, + block_size, + bits, + blocks: 0, + unfinished: None, + } + } +} + +impl Hasher for ParallelHash { + fn update(&mut self, mut input: &[u8]) { + if let Some(mut unfinished) = self.unfinished.take() { + let to_absorb = self.block_size - unfinished.absorbed; + if input.len() >= to_absorb { + unfinished.state.update(&input[..to_absorb]); + input = &input[to_absorb..]; + + let mut suboutput = Suboutout::security(self.bits); + unfinished.state.finalize(suboutput.as_bytes_mut()); + self.state.update(suboutput.as_bytes()); + self.blocks += 1; + } else { + unfinished.state.update(input); + unfinished.absorbed += input.len(); + self.unfinished = Some(unfinished); + return; + } + } + + let bits = self.bits; + let input_blocks_end = input.len() / self.block_size * self.block_size; + let input_blocks = &input[..input_blocks_end]; + let input_end = &input[input_blocks_end..]; + let parts = input_blocks.chunks(self.block_size).map(|chunk| { + let mut state = CShake::new(b"", b"", bits); + state.update(chunk); + let mut suboutput = Suboutout::security(bits); + state.finalize(suboutput.as_bytes_mut()); + suboutput + }); + + for part in parts { + self.state.update(part.as_bytes()); + self.blocks += 1; + } + + if !input_end.is_empty() { + assert!(self.unfinished.is_none()); + let mut state = CShake::new(b"", b"", bits); + state.update(input_end); + self.unfinished = Some(UnfinishedState { + state, + absorbed: input_end.len(), + }); + } + } + + fn finalize(mut self, output: &mut [u8]) { + if let Some(unfinished) = self.unfinished.take() { + let mut suboutput = Suboutout::security(self.bits); + unfinished.state.finalize(suboutput.as_bytes_mut()); + self.state.update(suboutput.as_bytes()); + self.blocks += 1; + } + + self.state.update(right_encode(self.blocks).value()); + self.state.update(right_encode(output.len() * 8).value()); + self.state.finalize(output); + } +} + +/// The `ParallelHashXOF` extendable-output functions defined in [`SP800-185`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["parallel_hash"] } +/// ``` +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::{ParallelHash, Xof, IntoXof, Hasher}; +/// let input = b"hello world"; +/// let mut output = [0u8; 64]; +/// let mut hasher = ParallelHash::v256(b"", 8); +/// hasher.update(input); +/// let mut xof = hasher.into_xof(); +/// xof.squeeze(&mut output[..32]); +/// xof.squeeze(&mut output[32..]); +/// ``` +/// +/// --- +/// +/// [`ParallelHashXof`] can be created only by using [`ParallelHash::IntoXof`] interface. +/// +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +/// [`ParallelHashXof`]: struct.ParallelHashXof.html +/// [`ParallelHash::IntoXof`]: struct.ParallelHash.html#impl-IntoXof +#[derive(Clone)] +pub struct ParallelHashXof { + state: CShake, +} + +impl IntoXof for ParallelHash { + type Xof = ParallelHashXof; + + fn into_xof(mut self) -> Self::Xof { + if let Some(unfinished) = self.unfinished.take() { + let mut suboutput = Suboutout::security(self.bits); + unfinished.state.finalize(suboutput.as_bytes_mut()); + self.state.update(suboutput.as_bytes()); + self.blocks += 1; + } + + self.state.update(right_encode(self.blocks).value()); + self.state.update(right_encode(0).value()); + + ParallelHashXof { state: self.state } + } +} + +impl Xof for ParallelHashXof { + fn squeeze(&mut self, output: &mut [u8]) { + self.state.squeeze(output); + } +} diff --git a/crypto/tiny-keccak/src/sha3.rs b/crypto/tiny-keccak/src/sha3.rs new file mode 100644 index 000000000..71326a274 --- /dev/null +++ b/crypto/tiny-keccak/src/sha3.rs @@ -0,0 +1,83 @@ +use crate::{bits_to_rate, keccakf::KeccakF, Hasher, KeccakState}; + +/// The `SHA3` hash functions defined in [`FIPS-202`]. +/// +/// [`FIPS-202`]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["sha3"] } +/// ``` +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::{Hasher, Sha3}; +/// # +/// # fn main() { +/// let input = b"hello world"; +/// let mut output = [0; 32]; +/// let expected = b"\ +/// \x64\x4b\xcc\x7e\x56\x43\x73\x04\x09\x99\xaa\xc8\x9e\x76\x22\xf3\ +/// \xca\x71\xfb\xa1\xd9\x72\xfd\x94\xa3\x1c\x3b\xfb\xf2\x4e\x39\x38\ +/// "; +/// let mut sha3 = Sha3::v256(); +/// sha3.update(input); +/// sha3.finalize(&mut output); +/// assert_eq!(expected, &output); +/// # } +/// ``` +#[derive(Clone)] +pub struct Sha3 { + state: KeccakState, +} + +impl Sha3 { + const DELIM: u8 = 0x06; + + /// Creates new [`Sha3`] hasher with a security level of 224 bits. + /// + /// [`Sha3`]: struct.Sha3.html + pub fn v224() -> Sha3 { + Sha3::new(224) + } + + /// Creates new [`Sha3`] hasher with a security level of 256 bits. + /// + /// [`Sha3`]: struct.Sha3.html + pub fn v256() -> Sha3 { + Sha3::new(256) + } + + /// Creates new [`Sha3`] hasher with a security level of 384 bits. + /// + /// [`Sha3`]: struct.Sha3.html + pub fn v384() -> Sha3 { + Sha3::new(384) + } + + /// Creates new [`Sha3`] hasher with a security level of 512 bits. + /// + /// [`Sha3`]: struct.Sha3.html + pub fn v512() -> Sha3 { + Sha3::new(512) + } + + fn new(bits: usize) -> Sha3 { + Sha3 { + state: KeccakState::new(bits_to_rate(bits), Self::DELIM), + } + } +} + +impl Hasher for Sha3 { + fn update(&mut self, input: &[u8]) { + self.state.update(input); + } + + fn finalize(self, output: &mut [u8]) { + self.state.finalize(output); + } +} diff --git a/crypto/tiny-keccak/src/shake.rs b/crypto/tiny-keccak/src/shake.rs new file mode 100644 index 000000000..fb9015e22 --- /dev/null +++ b/crypto/tiny-keccak/src/shake.rs @@ -0,0 +1,56 @@ +use crate::{bits_to_rate, keccakf::KeccakF, Hasher, KeccakState, Xof}; + +/// The `SHAKE` extendable-output functions defined in [`FIPS-202`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["shake"] } +/// ``` +/// +/// [`FIPS-202`]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.202.pdf +#[derive(Clone)] +pub struct Shake { + state: KeccakState, +} + +impl Shake { + const DELIM: u8 = 0x1f; + + /// Creates new [`Shake`] hasher with a security level of 128 bits. + /// + /// [`Shake`]: struct.Shake.html + pub fn v128() -> Shake { + Shake::new(128) + } + + /// Creates new [`Shake`] hasher with a security level of 256 bits. + /// + /// [`Shake`]: struct.Shake.html + pub fn v256() -> Shake { + Shake::new(256) + } + + pub(crate) fn new(bits: usize) -> Shake { + Shake { + state: KeccakState::new(bits_to_rate(bits), Self::DELIM), + } + } +} + +impl Hasher for Shake { + fn update(&mut self, input: &[u8]) { + self.state.update(input); + } + + fn finalize(self, output: &mut [u8]) { + self.state.finalize(output); + } +} + +impl Xof for Shake { + fn squeeze(&mut self, output: &mut [u8]) { + self.state.squeeze(output) + } +} diff --git a/crypto/tiny-keccak/src/tuple_hash.rs b/crypto/tiny-keccak/src/tuple_hash.rs new file mode 100644 index 000000000..a23ab14eb --- /dev/null +++ b/crypto/tiny-keccak/src/tuple_hash.rs @@ -0,0 +1,106 @@ +use crate::{left_encode, right_encode, CShake, Hasher, IntoXof, Xof}; + +/// The `TupleHash` hash functions defined in [`SP800-185`]. +/// +/// `TupleHash` is designed to provide a generic, misuse-resistant way to combine a sequence of +/// strings for hashing such that, for example, a `TupleHash` computed on the tuple (`"abc"` ,`"d"`) will +/// produce a different hash value than a `TupleHash` computed on the tuple (`"ab"`,`"cd"`), even though +/// all the remaining input parameters are kept the same, and the two resulting concatenated +/// strings, without string encoding, are identical. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["tuple_hash"] } +/// ``` +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +#[derive(Clone)] +pub struct TupleHash { + state: CShake, +} + +impl TupleHash { + /// Creates new [`TupleHash`] hasher with a security level of 128 bits. + /// + /// [`TupleHash`]: struct.TupleHash.html + pub fn v128(custom_string: &[u8]) -> TupleHash { + TupleHash::new(custom_string, 128) + } + + /// Creates new [`TupleHash`] hasher with a security level of 256 bits. + /// + /// [`TupleHash`]: struct.TupleHash.html + pub fn v256(custom_string: &[u8]) -> TupleHash { + TupleHash::new(custom_string, 256) + } + + fn new(custom_string: &[u8], bits: usize) -> TupleHash { + TupleHash { + state: CShake::new(b"TupleHash", custom_string, bits), + } + } +} + +impl Hasher for TupleHash { + fn update(&mut self, input: &[u8]) { + self.state.update(left_encode(input.len() * 8).value()); + self.state.update(input) + } + + fn finalize(mut self, output: &mut [u8]) { + self.state.update(right_encode(output.len() * 8).value()); + self.state.finalize(output) + } +} + +/// The `TupleHashXOF` extendable-output functions defined in [`SP800-185`]. +/// +/// # Usage +/// +/// ```toml +/// [dependencies] +/// tiny-keccak = { version = "2.0.0", features = ["tuple_hash"] } +/// ``` +/// +/// # Example +/// +/// ``` +/// # use tiny_keccak::{TupleHash, Xof, IntoXof, Hasher}; +/// let input = b"hello world"; +/// let mut output = [0u8; 64]; +/// let mut hasher = TupleHash::v256(b""); +/// hasher.update(input); +/// let mut xof = hasher.into_xof(); +/// xof.squeeze(&mut output[..32]); +/// xof.squeeze(&mut output[32..]); +/// ``` +/// +/// --- +/// +/// [`TupleHashXof`] can be created only by using [`TupleHash::IntoXof`] interface. +/// +/// +/// [`SP800-185`]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-185.pdf +/// [`TupleHashXof`]: struct.TupleHashXof.html +/// [`TupleHash::IntoXof`]: struct.TupleHash.html#impl-IntoXof +#[derive(Clone)] +pub struct TupleHashXof { + state: CShake, +} + +impl IntoXof for TupleHash { + type Xof = TupleHashXof; + + fn into_xof(mut self) -> TupleHashXof { + self.state.update(right_encode(0).value()); + TupleHashXof { state: self.state } + } +} + +impl Xof for TupleHashXof { + fn squeeze(&mut self, output: &mut [u8]) { + self.state.squeeze(output) + } +} diff --git a/executor/Cargo.toml b/executor/Cargo.toml index 3f278e1c6..fb890e353 100644 --- a/executor/Cargo.toml +++ b/executor/Cargo.toml @@ -8,6 +8,9 @@ license.workspace = true thiserror = "1.0.68" rustc-demangle = "0.1" ecsm = { path = "../crypto/ecsm" } +# Host-side computation of non-constraining hints (modular inverse / sqrt) for the +# `Hint` ecall — same k256 arithmetic the guest verifies against. BENCH ONLY. +k256 = { version = "0.13", default-features = false, features = ["arithmetic", "expose-field"] } [dev-dependencies] serde = { version = "1.0", features = ["derive"] } diff --git a/executor/programs/bench/ecsm/Cargo.lock b/executor/programs/bench/ecsm/Cargo.lock index 9e09ad93d..b5afa66a3 100644 --- a/executor/programs/bench/ecsm/Cargo.lock +++ b/executor/programs/bench/ecsm/Cargo.lock @@ -26,6 +26,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "ecsm" version = "0.1.0" @@ -78,6 +89,8 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ + "critical-section", + "dlmalloc", "embedded-alloc", "getrandom 0.2.17", "getrandom 0.3.4", @@ -304,6 +317,21 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/executor/programs/bench/hashmap/Cargo.lock b/executor/programs/bench/hashmap/Cargo.lock index 217419bfd..413570ece 100644 --- a/executor/programs/bench/hashmap/Cargo.lock +++ b/executor/programs/bench/hashmap/Cargo.lock @@ -26,6 +26,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "embedded-alloc" version = "0.6.0" @@ -78,6 +89,8 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ + "critical-section", + "dlmalloc", "embedded-alloc", "getrandom 0.2.17", "getrandom 0.3.4", @@ -297,6 +310,21 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/executor/programs/bench/keccak/Cargo.lock b/executor/programs/bench/keccak/Cargo.lock index 8419d2cc3..696f724da 100644 --- a/executor/programs/bench/keccak/Cargo.lock +++ b/executor/programs/bench/keccak/Cargo.lock @@ -32,6 +32,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "embedded-alloc" version = "0.6.0" @@ -85,6 +96,8 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ + "critical-section", + "dlmalloc", "embedded-alloc", "getrandom 0.2.17", "getrandom 0.3.4", @@ -313,6 +326,21 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.46.0" diff --git a/executor/programs/bench/syscall_commit/Cargo.lock b/executor/programs/bench/syscall_commit/Cargo.lock index a02ade5fa..bd792d70a 100644 --- a/executor/programs/bench/syscall_commit/Cargo.lock +++ b/executor/programs/bench/syscall_commit/Cargo.lock @@ -26,6 +26,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "embedded-alloc" version = "0.6.0" @@ -71,6 +82,8 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ + "critical-section", + "dlmalloc", "embedded-alloc", "getrandom 0.2.17", "getrandom 0.3.4", @@ -297,6 +310,21 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/executor/programs/rust/ethrex/Cargo.lock b/executor/programs/rust/ethrex/Cargo.lock index e1674f74f..a90a0f7d3 100644 --- a/executor/programs/rust/ethrex/Cargo.lock +++ b/executor/programs/rust/ethrex/Cargo.lock @@ -2210,10 +2210,9 @@ dependencies = [ [[package]] name = "tiny-keccak" version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" dependencies = [ "crunchy", + "lambda-vm-syscalls", ] [[package]] diff --git a/executor/programs/rust/ethrex/Cargo.toml b/executor/programs/rust/ethrex/Cargo.toml index 7d3ed7114..ab4dcac5a 100644 --- a/executor/programs/rust/ethrex/Cargo.toml +++ b/executor/programs/rust/ethrex/Cargo.toml @@ -28,3 +28,10 @@ rkyv = { version = "=0.8.16", features = ["std", "unaligned"] } # current syscalls (keccak_permute + the Print-ecall no-op fix). [patch."https://github.com/yetanotherco/lambda_vm.git"] lambda-vm-syscalls = { path = "../../../../syscalls" } + +# BENCH: route every Keccak permutation (Ethereum trie/RLP hashing via ethrex-crypto's +# `keccak_hash` + `ethbloom`) through the `keccak_permute` accelerator syscall by +# swapping in our vendored tiny-keccak fork (only `keccakf` changes; host builds +# fall back to the software permutation). +[patch.crates-io] +tiny-keccak = { path = "../../../../crypto/tiny-keccak" } diff --git a/executor/programs/rust/ethrex/src/main.rs b/executor/programs/rust/ethrex/src/main.rs index 30a39f4b5..2786cf3f6 100644 --- a/executor/programs/rust/ethrex/src/main.rs +++ b/executor/programs/rust/ethrex/src/main.rs @@ -5,16 +5,19 @@ use lambda_vm_ethrex_crypto::LambdaVmEcsmCrypto; use rkyv::rancor::Error; pub fn main() { - let input = lambda_vm_syscalls::syscalls::get_private_input(); - let input = rkyv::from_bytes::(&input).unwrap(); + // Borrow the private input in place from the memory-mapped region (no owned-Vec + // copy); rkyv reads the archive directly out of it. The `unaligned` rkyv feature + // (see Cargo.toml) makes `from_bytes` tolerate the region's byte alignment. + let input = lambda_vm_syscalls::syscalls::get_private_input_slice(); + let input = rkyv::from_bytes::(input).unwrap(); // LambdaVM crypto provider, defined in the lambda_vm repo and injected here // (so crypto changes don't require an ethrex PR — see `crypto/ethrex-crypto`). // It accelerates trait-routed `keccak256` (via the keccak_permute precompile) // and `secp256k1_ecrecover` (via the ECSM precompile); everything else uses - // ethrex's pure-Rust trait defaults. ethrex's trie/RLP keccak that goes - // through the free `keccak_hash` fn is still software, and KZG (0x0a) is - // unsupported under the `lambdavm` feature (blob txs execute; a point-eval - // precompile call reverts). + // ethrex's pure-Rust trait defaults. ethrex's trie/RLP keccak now also routes + // through the keccak_permute precompile via a vendored tiny-keccak patch (see + // Cargo.toml). KZG (0x0a) is unsupported under the `lambdavm` feature (blob txs + // execute; a point-eval precompile call reverts). let crypto = Arc::new(LambdaVmEcsmCrypto); let output = execution_program(input, crypto).unwrap(); lambda_vm_syscalls::syscalls::commit(&output.encode()); diff --git a/executor/programs/rust/hint_min/.cargo/config.toml b/executor/programs/rust/hint_min/.cargo/config.toml new file mode 100644 index 000000000..8ef8239bb --- /dev/null +++ b/executor/programs/rust/hint_min/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "--cfg", "getrandom_backend=\"custom\"", + "-C", "passes=lower-atomic" +] + +[env] +CC_riscv64im_lambda_vm_elf = "clang" +CFLAGS_riscv64im_lambda_vm_elf = "--target=riscv64 -march=rv64im -mabi=lp64 --sysroot=/opt/lambda-vm-sysroot" diff --git a/executor/programs/rust/hint_min/Cargo.lock b/executor/programs/rust/hint_min/Cargo.lock new file mode 100644 index 000000000..cc02eff98 --- /dev/null +++ b/executor/programs/rust/hint_min/Cargo.lock @@ -0,0 +1,331 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "embedded-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" +dependencies = [ + "const-default", + "critical-section", + "linked_list_allocator", + "rlsf", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hint_min" +version = "0.1.0" +dependencies = [ + "lambda-vm-syscalls", +] + +[[package]] +name = "lambda-vm-syscalls" +version = "0.1.0" +dependencies = [ + "embedded-alloc", + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "rand", + "riscv", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linked_list_allocator" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "rlsf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" +dependencies = [ + "cfg-if", + "const-default", + "libc", + "rustversion", + "svgbobdoc", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "svgbobdoc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" +dependencies = [ + "base64", + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-width", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/executor/programs/rust/hint_min/Cargo.toml b/executor/programs/rust/hint_min/Cargo.toml new file mode 100644 index 000000000..4bfe4614f --- /dev/null +++ b/executor/programs/rust/hint_min/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] + +[package] +name = "hint_min" +version = "0.1.0" +edition = "2024" + +[dependencies] +lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/programs/rust/hint_min/src/main.rs b/executor/programs/rust/hint_min/src/main.rs new file mode 100644 index 000000000..f736a6706 --- /dev/null +++ b/executor/programs/rust/hint_min/src/main.rs @@ -0,0 +1,26 @@ +//! Minimal P0 guest for the Hint prover table: one `hint` ecall (field inverse of +//! a small value) + commit the result. No in-guest verify — this exercises exactly +//! the Hint table's bus surface (Ecall receive + one 32-byte MEMW read + one 32-byte +//! MEMW write) so we can get prove→verify to balance before scaling to ethrex. +//! +//! Buffers are 8-byte aligned so the MEMW accesses land in the aligned MEMW table. + +use lambda_vm_syscalls as syscalls; + +#[repr(align(8))] +struct Aligned32([u8; 32]); + +pub fn main() { + // input = 3 (big-endian), a valid invertible field element. + let mut x = Aligned32([0u8; 32]); + x.0[31] = 3; + let mut inv = Aligned32([0u8; 32]); + + syscalls::syscalls::hint( + syscalls::syscalls::HINT_FIELD_INV, + &mut inv.0, + &x.0, + ); + + syscalls::syscalls::commit(&inv.0); +} diff --git a/executor/programs/rust/hint_multi/.cargo/config.toml b/executor/programs/rust/hint_multi/.cargo/config.toml new file mode 100644 index 000000000..8ef8239bb --- /dev/null +++ b/executor/programs/rust/hint_multi/.cargo/config.toml @@ -0,0 +1,9 @@ +[target.riscv64im-lambda-vm-elf] +rustflags = [ + "--cfg", "getrandom_backend=\"custom\"", + "-C", "passes=lower-atomic" +] + +[env] +CC_riscv64im_lambda_vm_elf = "clang" +CFLAGS_riscv64im_lambda_vm_elf = "--target=riscv64 -march=rv64im -mabi=lp64 --sysroot=/opt/lambda-vm-sysroot" diff --git a/executor/programs/rust/hint_multi/Cargo.lock b/executor/programs/rust/hint_multi/Cargo.lock new file mode 100644 index 000000000..9803c875a --- /dev/null +++ b/executor/programs/rust/hint_multi/Cargo.lock @@ -0,0 +1,331 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const-default" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b396d1f76d455557e1218ec8066ae14bba60b4b36ecd55577ba979f5db7ecaa" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "embedded-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f2de9133f68db0d4627ad69db767726c99ff8585272716708227008d3f1bddd" +dependencies = [ + "const-default", + "critical-section", + "linked_list_allocator", + "rlsf", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hint_multi" +version = "0.1.0" +dependencies = [ + "lambda-vm-syscalls", +] + +[[package]] +name = "lambda-vm-syscalls" +version = "0.1.0" +dependencies = [ + "embedded-alloc", + "getrandom 0.2.17", + "getrandom 0.3.4", + "lazy_static", + "rand", + "riscv", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linked_list_allocator" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "riscv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05cfa3f7b30c84536a9025150d44d26b8e1cc20ddf436448d74cd9591eefb25" +dependencies = [ + "critical-section", + "embedded-hal", + "paste", + "riscv-macros", + "riscv-pac", +] + +[[package]] +name = "riscv-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "riscv-pac" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" + +[[package]] +name = "rlsf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" +dependencies = [ + "cfg-if", + "const-default", + "libc", + "rustversion", + "svgbobdoc", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "svgbobdoc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" +dependencies = [ + "base64", + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-width", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/executor/programs/rust/hint_multi/Cargo.toml b/executor/programs/rust/hint_multi/Cargo.toml new file mode 100644 index 000000000..faacdb38e --- /dev/null +++ b/executor/programs/rust/hint_multi/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] + +[package] +name = "hint_multi" +version = "0.1.0" +edition = "2024" + +[dependencies] +lambda-vm-syscalls = { path = "../../../../syscalls" } diff --git a/executor/programs/rust/hint_multi/src/main.rs b/executor/programs/rust/hint_multi/src/main.rs new file mode 100644 index 000000000..67f2ab90f --- /dev/null +++ b/executor/programs/rust/hint_multi/src/main.rs @@ -0,0 +1,35 @@ +//! Multi-hint P0/P2 guest for the Hint prover table: THREE `hint` ecalls +//! (field inverse of three different small values), each result read back with +//! ordinary `LOAD`s (XOR-accumulated) and the accumulator committed. +//! +//! Complements `hint_min` (one hint, read back via `commit`): this exercises the +//! parts the ethrex consumer relies on that a single-call guest does not — +//! **multiple real HINT rows** (padded to a power of two) and **read-back of the +//! hinted output via normal `LOAD` instructions** (whose MEMW reads must chain to +//! the HINT table's writes). Buffers are 8-byte aligned so the writes land in the +//! aligned MEMW table. + +use lambda_vm_syscalls as syscalls; + +#[repr(align(8))] +struct Aligned32([u8; 32]); + +pub fn main() { + let mut acc = Aligned32([0u8; 32]); + + for seed in [3u8, 5u8, 7u8] { + let mut x = Aligned32([0u8; 32]); + x.0[31] = seed; + let mut inv = Aligned32([0u8; 32]); + + syscalls::syscalls::hint(syscalls::syscalls::HINT_FIELD_INV, &mut inv.0, &x.0); + + // Read the hinted output back via ordinary loads and fold it in, so the + // MEMW reads of `inv` must chain to the HINT table's writes. + for i in 0..32 { + acc.0[i] ^= inv.0[i]; + } + } + + syscalls::syscalls::commit(&acc.0); +} diff --git a/executor/src/tests/hint_tests.rs b/executor/src/tests/hint_tests.rs new file mode 100644 index 000000000..3b0cb8aae --- /dev/null +++ b/executor/src/tests/hint_tests.rs @@ -0,0 +1,118 @@ +//! Tests for the non-constraining `Hint` syscall (BENCH ONLY). + +use crate::vm::instruction::decoding::Instruction; +use crate::vm::instruction::execution::{ + ExecutionError, HINT_FIELD_INV, HINT_SYSCALL_NUMBER, compute_hint, +}; +use crate::vm::memory::Memory; +use crate::vm::registers::Registers; + +fn write_u256(memory: &mut Memory, addr: u64, bytes: &[u8; 32]) { + for i in 0..4 { + let mut dw = [0u8; 8]; + dw.copy_from_slice(&bytes[i * 8..i * 8 + 8]); + memory + .store_doubleword(addr + (i as u64) * 8, u64::from_le_bytes(dw)) + .unwrap(); + } +} + +fn read_u256(memory: &Memory, addr: u64) -> [u8; 32] { + let mut out = [0u8; 32]; + for i in 0..4 { + let dw = memory.load_doubleword(addr + (i as u64) * 8).unwrap(); + out[i * 8..i * 8 + 8].copy_from_slice(&dw.to_le_bytes()); + } + out +} + +/// Runs one `Hint` ecall with the given operand addresses, returning the 32 bytes +/// written at `out_addr`. +fn run_hint_at( + hint_id: u64, + in_addr: u64, + out_addr: u64, + input: &[u8; 32], +) -> Result<[u8; 32], ExecutionError> { + let mut memory = Memory::default(); + let mut registers = Registers::default(); + let mut pc = 0u64; + + write_u256(&mut memory, in_addr, input); + registers.write(17, HINT_SYSCALL_NUMBER).unwrap(); + registers.write(10, hint_id).unwrap(); + registers.write(11, in_addr).unwrap(); + registers.write(12, out_addr).unwrap(); + Instruction::EcallEbreak.run(&mut pc, &mut registers, &mut memory)?; + Ok(read_u256(&memory, out_addr)) +} + +/// The base-field inverse hint round-trips through guest memory, big-endian in and +/// out, and matches `compute_hint` (the value the prover recomputes). +#[test] +fn hint_syscall_writes_the_field_inverse() { + let mut input = [0u8; 32]; + input[31] = 3; // 3, big-endian + + let out = run_hint_at(HINT_FIELD_INV, 0x1000, 0x2000, &input).expect("hint must run"); + assert_eq!(out, compute_hint(HINT_FIELD_INV, &input)); + + // 3 · 3⁻¹ ≡ 1 (mod p) — the same check the guest performs on the untrusted value. + let three: k256::FieldElement = + Option::from(k256::FieldElement::from_bytes(&input.into())).unwrap(); + let inv: k256::FieldElement = + Option::from(k256::FieldElement::from_bytes(&out.into())).unwrap(); + assert_eq!( + (three * inv).to_bytes(), + k256::FieldElement::ONE.to_bytes(), + "hinted inverse must satisfy x·inv == 1" + ); +} + +/// Both operands must keep their 32-byte range inside the lower address limb: the +/// HINT table sends the output writes as `[out_addr_lo + 8i, out_addr_hi]`, which +/// cannot represent a carry into the high limb, so a straddling operand would make +/// the trace unprovable. The executor rejects it upfront instead. +#[test] +fn hint_syscall_rejects_address_overflow() { + let input = [0u8; 32]; + // Last accessed byte is at +31, so the first rejected base is 2^32 - 31. + for (in_addr, out_addr) in [ + (0x1000, 0xFFFF_FFE8), + (0xFFFF_FFE8, 0x2000), + (0x1000, 0xFFFF_FFE1), + (0xFFFF_FFE1, 0x2000), + (0x1000, 0xFFFF_FFFF), + ] { + let err = run_hint_at(HINT_FIELD_INV, in_addr, out_addr, &input) + .expect_err("straddling operand must be rejected"); + assert!( + matches!(err, ExecutionError::HintAddressOverflow), + "expected address overflow for in={in_addr:#x}, out={out_addr:#x}, got {err:?}" + ); + } +} + +/// The boundary case: an operand ending exactly on the last byte of the limb is +/// still representable and must be accepted. +#[test] +fn hint_syscall_accepts_operand_ending_at_the_limb_boundary() { + let input = [0u8; 32]; + // 2^32 - 32: last byte lands at 2^32 - 1, the largest in-limb address. + run_hint_at(HINT_FIELD_INV, 0x1000, 0xFFFF_FFE0, &input) + .expect("operand ending at the limb boundary must run"); + run_hint_at(HINT_FIELD_INV, 0xFFFF_FFE0, 0x2000, &input) + .expect("operand ending at the limb boundary must run"); +} + +/// An unknown `hint_id` is not an error — the ecall writes zeros and the guest's +/// verify is what rejects the value. Pins that contract so a future selector can't +/// silently start trapping instead. +#[test] +fn hint_syscall_writes_zeros_for_an_unknown_selector() { + let mut input = [0u8; 32]; + input[31] = 3; + let out = + run_hint_at(u64::MAX, 0x1000, 0x2000, &input).expect("unknown selector must not trap"); + assert_eq!(out, [0u8; 32]); +} diff --git a/executor/src/tests/mod.rs b/executor/src/tests/mod.rs index 456607433..244447b22 100644 --- a/executor/src/tests/mod.rs +++ b/executor/src/tests/mod.rs @@ -1,4 +1,5 @@ pub mod ecsm_tests; pub mod flamegraph_tests; +pub mod hint_tests; pub mod keccak_tests; pub mod memory_tests; diff --git a/executor/src/vm/instruction/execution.rs b/executor/src/vm/instruction/execution.rs index c92c0ab88..87c636d64 100644 --- a/executor/src/vm/instruction/execution.rs +++ b/executor/src/vm/instruction/execution.rs @@ -16,6 +16,9 @@ pub enum SyscallNumbers { Halt = 93, // Placeholder discriminant. The actual syscall value is ECSM_SYSCALL_NUMBER. Ecsm = 94, + // Placeholder discriminant. The actual syscall value is HINT_SYSCALL_NUMBER. + // BENCH ONLY: non-constraining hint (host computes modular inverse/sqrt, guest verifies). + Hint = 95, } /// Syscall number for KeccakPermute (u64::MAX - 1 = 0xFFFF_FFFF_FFFF_FFFE). @@ -31,6 +34,19 @@ const KECCAK_STATE_BYTES: u64 = 25 * 8; /// bus as `[lo32, hi32] = [2^32 - 11, 2^32 - 1]`. pub const ECSM_SYSCALL_NUMBER: u64 = u64::MAX - 10; +/// Syscall number for the non-constraining `Hint` ecall (BENCH ONLY). +/// +/// The host computes a modular inverse or square root and writes it back to the +/// guest, which must verify it (e.g. `x·inv == 1`). This adds no in-circuit +/// correctness constraint of its own — it exists to measure the cost of the +/// hint-then-verify pattern versus computing the operation in the guest. +pub const HINT_SYSCALL_NUMBER: u64 = u64::MAX - 20; + +/// Hint operation selector passed in `a0`. +pub const HINT_FIELD_INV: u64 = 0; // secp256k1 base-field inverse (mod p) +pub const HINT_SCALAR_INV: u64 = 1; // secp256k1 scalar-field inverse (mod n) +pub const HINT_FIELD_SQRT: u64 = 2; // secp256k1 base-field square root + /// `2^32`. ECSM memory operands must not overflow their lower 32-bit address limb when the /// largest per-access offset is added: the 32-byte operands reach offset +31 (last byte). const LOW_LIMB: u64 = 1 << 32; @@ -45,6 +61,7 @@ impl TryFrom for SyscallNumbers { 93 => Ok(SyscallNumbers::Halt), v if v == KECCAK_SYSCALL_NUMBER => Ok(SyscallNumbers::KeccakPermute), v if v == ECSM_SYSCALL_NUMBER => Ok(SyscallNumbers::Ecsm), + v if v == HINT_SYSCALL_NUMBER => Ok(SyscallNumbers::Hint), _ => Err(()), } } @@ -68,7 +85,8 @@ impl SyscallNumbers { SyscallNumbers::Print | SyscallNumbers::Panic | SyscallNumbers::Commit - | SyscallNumbers::Halt => None, + | SyscallNumbers::Halt + | SyscallNumbers::Hint => None, } } } @@ -93,8 +111,53 @@ fn store_u256_le(memory: &mut Memory, addr: u64, bytes: &[u8; 32]) -> Result<(), Ok(()) } -/// Checks the ECSM address-alignment assumption: `(addr mod 2^32) + max_offset < 2^32`. -fn ecsm_addr_ok(addr: u64, max_offset: u64) -> bool { +/// BENCH ONLY. Compute a non-constraining hint (modular inverse / sqrt) with the +/// same k256 arithmetic the guest verifies against. Input/output are 32-byte +/// big-endian, k256's own serialization — unlike the ECSM ABI, which is +/// little-endian because its chip consumes little-endian limbs. The HINT table +/// only copies these bytes into memory writes, so the order is free to match the +/// consumers. On any failure (non-canonical input, no inverse/sqrt) returns zeros; +/// the guest's verify then fails loudly. +/// +/// `pub` so the prover's `collect_hint_ops` can reproduce the exact output value +/// the executor wrote to guest memory (the value is not carried in the CPU log). +pub fn compute_hint(hint_id: u64, in_be: &[u8; 32]) -> [u8; 32] { + use k256::elliptic_curve::PrimeField; + let mut fb = k256::FieldBytes::default(); + fb.copy_from_slice(in_be); + + match hint_id { + HINT_FIELD_INV => { + let x: Option = Option::from(k256::FieldElement::from_bytes(&fb)); + match x.and_then(|x| Option::::from(x.invert())) { + Some(inv) => inv.to_bytes().into(), + None => [0u8; 32], + } + } + HINT_SCALAR_INV => { + let x: Option = Option::from(k256::Scalar::from_repr(fb)); + match x.and_then(|x| Option::::from(x.invert())) { + Some(inv) => inv.to_bytes().into(), + None => [0u8; 32], + } + } + HINT_FIELD_SQRT => { + let x: Option = Option::from(k256::FieldElement::from_bytes(&fb)); + match x.and_then(|x| Option::::from(x.sqrt())) { + Some(r) => r.to_bytes().into(), + None => [0u8; 32], + } + } + _ => [0u8; 32], + } +} + +/// Checks that a 32-byte operand does not overflow its lower 32-bit address limb: +/// `(addr mod 2^32) + max_offset < 2^32`. Tables that send an address to the memory +/// bus as a `[lo32, hi32]` pair with the per-access offset added to `lo32` alone +/// cannot represent a carry into `hi32`, so an operand straddling the limb boundary +/// makes the trace unprovable. Used by the ECSM and Hint ecalls. +fn addr_limb_ok(addr: u64, max_offset: u64) -> bool { (addr % LOW_LIMB) + max_offset < LOW_LIMB } @@ -429,9 +492,9 @@ impl Instruction { let addr_xr = registers.read(10)?; let addr_xg = registers.read(11)?; let addr_k = registers.read(12)?; - if !ecsm_addr_ok(addr_xg, 31) - || !ecsm_addr_ok(addr_xr, 31) - || !ecsm_addr_ok(addr_k, 31) + if !addr_limb_ok(addr_xg, 31) + || !addr_limb_ok(addr_xr, 31) + || !addr_limb_ok(addr_k, 31) { return Err(ExecutionError::EcsmAddressOverflow); } @@ -454,6 +517,30 @@ impl Instruction { src2_val = addr_xg; dst_val = addr_k; } + SyscallNumbers::Hint => { + // BENCH ONLY. Non-constraining hint: host computes a modular + // inverse/sqrt and writes it to the guest, which verifies it. + // a0 = hint_id, a1 = input addr (32-byte BE), a2 = output addr. + // The `_le` helpers only move bytes in address order, which is + // what a raw big-endian buffer needs. + let hint_id = registers.read(10)?; + let in_addr = registers.read(11)?; + let out_addr = registers.read(12)?; + // The HINT table sends the output writes as `[out_addr_lo + 8i, + // out_addr_hi]`, so an `out_addr` whose 32-byte range crosses the + // limb boundary would unbalance the memory bus. `in_addr` is not on + // the bus (the input read is not modeled) but is bounded too, so the + // ecall's operand contract is uniform and `load_u256_le` cannot + // overflow its address arithmetic. + if !addr_limb_ok(in_addr, 31) || !addr_limb_ok(out_addr, 31) { + return Err(ExecutionError::HintAddressOverflow); + } + let input = load_u256_le(memory, in_addr)?; + let output = compute_hint(hint_id, &input); + store_u256_le(memory, out_addr, &output)?; + src2_val = in_addr; + dst_val = out_addr; + } SyscallNumbers::Halt => { // halt return Ok(Log { @@ -634,6 +721,8 @@ pub enum ExecutionError { EcsmAddressOverflow, #[error("ECSM xG and k operand ranges overlap")] EcsmOperandOverlap, + #[error("Hint address range overflows the lower 32-bit limb")] + HintAddressOverflow, #[error("ECSM scalar multiplication error: {0}")] Ecsm(#[from] ecsm::EcsmError), } diff --git a/prover/src/lib.rs b/prover/src/lib.rs index a8e89f989..359e9c16b 100644 --- a/prover/src/lib.rs +++ b/prover/src/lib.rs @@ -53,8 +53,8 @@ use crate::tables::types::BusId; use crate::test_utils::{ E, F, VmAir, create_bitwise_air, create_branch_air, create_bytewise_air, create_commit_air, create_cpu_air, create_cpu32_air, create_decode_air, create_dvrm_air, create_ecdas_air, - create_ecsm_air, create_eq_air, create_halt_air, create_keccak_air, create_keccak_rc_air, - create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, + create_ecsm_air, create_eq_air, create_halt_air, create_hint_air, create_keccak_air, + create_keccak_rc_air, create_keccak_rnd_air, create_load_air, create_lt_air, create_memw_air, create_memw_aligned_air, create_memw_register_air, create_mul_air, create_page_air, create_register_air, create_shift_air, create_store_air, }; @@ -82,8 +82,8 @@ pub struct RuntimePageRange { /// Number of tables that always contribute exactly one sub-proof, regardless /// of `TableCounts`: bitwise, decode, halt, commit, keccak, keccak_rnd, -/// keccak_rc, register, ecsm, ecdas. -pub const FIXED_TABLE_COUNT: usize = 10; +/// keccak_rc, register, ecsm, ecdas, hint. +pub const FIXED_TABLE_COUNT: usize = 11; /// Number of chunks for each split table. /// The verifier needs this to reconstruct matching AIRs. @@ -517,6 +517,7 @@ pub(crate) struct VmAirs { pub keccak_rc: VmAir, pub ecsm: VmAir, pub ecdas: VmAir, + pub hint: VmAir, pub register: VmAir, pub pages: Vec, pub memw_registers: Vec, @@ -542,6 +543,7 @@ impl VmAirs { (self.keccak_rc.as_ref(), &mut traces.keccak_rc, &()), (self.ecsm.as_ref(), &mut traces.ecsm, &()), (self.ecdas.as_ref(), &mut traces.ecdas, &()), + (self.hint.as_ref(), &mut traces.hint, &()), (self.register.as_ref(), &mut traces.register, &()), ]; if self.include_halt { @@ -616,6 +618,7 @@ impl VmAirs { self.keccak_rc.as_ref(), self.ecsm.as_ref(), self.ecdas.as_ref(), + self.hint.as_ref(), self.register.as_ref(), ]; if self.include_halt { @@ -773,6 +776,7 @@ impl VmAirs { )); let ecsm: VmAir = Box::new(create_ecsm_air(proof_options)); let ecdas: VmAir = Box::new(create_ecdas_air(proof_options)); + let hint: VmAir = Box::new(create_hint_air(proof_options)); let register: VmAir = if let Some((commitment, num_preprocessed_cols)) = register_preprocessed { Box::new( @@ -879,6 +883,7 @@ impl VmAirs { keccak_rc, ecsm, ecdas, + hint, register, pages, memw_registers, diff --git a/prover/src/tables/cpu.rs b/prover/src/tables/cpu.rs index 781bb02b0..0bb29aaf1 100644 --- a/prover/src/tables/cpu.rs +++ b/prover/src/tables/cpu.rs @@ -188,6 +188,11 @@ pub struct CpuOperation { /// Whether this ECALL is an ECSM (elliptic-curve scalar multiply) syscall pub ecall_ecsm: bool, + + /// Whether this ECALL is a non-constraining Hint syscall (BENCH ONLY). The + /// hint operand addresses (x10/x11/x12) are recovered from the register state + /// in the trace builder, exactly like ECSM. + pub ecall_hint: bool, } impl CpuOperation { @@ -235,6 +240,8 @@ impl CpuOperation { // in the trace builder. let ecall_ecsm = f.ecall && log.src1_val == executor::vm::instruction::execution::ECSM_SYSCALL_NUMBER; + let ecall_hint = + f.ecall && log.src1_val == executor::vm::instruction::execution::HINT_SYSCALL_NUMBER; // Word instructions are fully handled by CPU32; the main CPU row is a // delegate that only advances the PC and sends the CPU32 lookup. We still @@ -353,6 +360,7 @@ impl CpuOperation { ecall_keccak, keccak_state_addr, ecall_ecsm, + ecall_hint, } } diff --git a/prover/src/tables/hint.rs b/prover/src/tables/hint.rs new file mode 100644 index 000000000..55edc4fe0 --- /dev/null +++ b/prover/src/tables/hint.rs @@ -0,0 +1,171 @@ +//! HINT table — receiver for the non-constraining `hint` ecall (BENCH ONLY). +//! +//! The `hint` ecall (syscall `u64::MAX - 20`) lets the executor hand the guest a +//! value that is expensive to compute but cheap to verify (modular inverse, sqrt, +//! …); the guest verifies it with ordinary constrained instructions. Unlike a +//! normal `STORE`, the ecall writes the 32-byte output to guest memory *directly* +//! (not through the CPU load/store decode), so those writes are invisible to the +//! CPU op stream — this table is what puts them into the memory argument. +//! +//! The table therefore does exactly two things, and constrains **nothing** about +//! the hinted value (that is the point — soundness lives in the guest's verify): +//! +//! 1. **Receives** the `Hint` ecall on the `Ecall` bus (balances the CPU's send; +//! a syscall with no receiver leaves the LogUp argument unbalanced). +//! 2. **Sends** the four 8-byte MEMW writes of the output at `out_addr` +0/8/16/24 +//! (received by the MEMW table). Without these the output's initial→final +//! memory chain is unexplained and the memory argument fails to balance. +//! +//! The input read (the ecall also reads `in_addr`) is intentionally **not** modeled: +//! a read leaves the value unchanged, the guest supplies the input via ordinary +//! stores, and nothing depends on the ecall having re-read it — so omitting it is +//! sound and avoids the mixed-timestamp bookkeeping of a partial-buffer read. +//! +//! ## Columns (37) +//! - `timestamp[0..1]` (DWordWL): the ecall timestamp `T` +//! - `out_addr[0..1]` (DWordWL): base address of the 32-byte output buffer +//! - `out_bytes[0..31]`: the 32 output bytes (the hint) — **unconstrained** +//! - `mu`: multiplicity flag (1 = real hint call, 0 = padding) — gates every bus + +use stark::lookup::{BusInteraction, BusValue, LinearTerm, Multiplicity, Packing}; +use stark::trace::TraceTable; + +use super::types::{BusId, FE, GoldilocksExtension, GoldilocksField, VmTable}; + +/// `hint` ecall syscall number — must match +/// `executor::vm::instruction::execution::HINT_SYSCALL_NUMBER`. +const HINT_SYSCALL_NUMBER: u64 = u64::MAX - 20; + +pub mod cols { + /// timestamp[0]: lower 32 bits of the ecall timestamp + pub const TIMESTAMP_0: usize = 0; + /// timestamp[1]: upper 32 bits (always 0 — timestamps fit u32) + pub const TIMESTAMP_1: usize = 1; + /// out_addr[0]: lower 32 bits of the output base address + pub const ADDR_OUT_0: usize = 2; + /// out_addr[1]: upper 32 bits of the output base address + pub const ADDR_OUT_1: usize = 3; + /// out_bytes[0..31]: the 32 output bytes, one per column + pub const OUT: usize = 4; + /// multiplicity flag (1 = real hint call, 0 = padding) + pub const MU: usize = 36; + + pub const NUM_COLUMNS: usize = 37; + + /// Column of output byte `i` (0..32). + #[inline] + pub const fn out(i: usize) -> usize { + OUT + i + } +} + +/// One `hint` ecall: the timestamp, the output base address, and the 32 output +/// bytes the executor wrote to guest memory (recomputed by the trace builder). +#[derive(Debug, Clone)] +pub struct HintOperation { + pub timestamp: u64, + pub out_addr: u64, + pub out_bytes: [u8; 32], +} + +/// Generates the HINT trace: one row per hint-ecall call (in program order), +/// `mu = 1`; padding rows are all-zero (`mu = 0`, inert on the bus). Empty (all +/// padding) for programs that make no hint calls. +pub fn generate_hint_trace( + ops: &[HintOperation], +) -> TraceTable { + let num_rows = ops.len().next_power_of_two().max(4); + let mut trace = TraceTable::new_main( + crate::tables::types::zeroed_fe_vec(num_rows * cols::NUM_COLUMNS), + cols::NUM_COLUMNS, + 1, + ); + let table = &mut trace.main_table; + + for (row, op) in ops.iter().enumerate() { + debug_assert!( + op.timestamp <= u32::MAX as u64, + "HINT timestamp {} exceeds u32", + op.timestamp + ); + table.set_dword_wl(row, cols::TIMESTAMP_0, op.timestamp); + table.set_dword_wl(row, cols::ADDR_OUT_0, op.out_addr); + table.set_bytes(row, cols::OUT, &op.out_bytes); + table.set_fe(row, cols::MU, FE::one()); + } + + trace +} + +// ========================================================================= +// Bus interactions +// ========================================================================= + +fn packed(col: usize) -> BusValue { + BusValue::Packed { + start_column: col, + packing: Packing::Direct, + } +} + +/// The eight output bytes of doubleword `chunk` (`out_bytes[8*chunk .. 8*chunk+7]`) +/// as MEMW value elements. +fn out_dword_bytes(chunk: usize) -> [BusValue; 8] { + std::array::from_fn(|b| packed(cols::out(8 * chunk + b))) +} + +/// A 16-element MEMW **write** tuple (CO25): `[is_register=0, base_lo, base_hi, +/// value[8], ts_lo, ts_hi, w2=0, w4=0, w8=1]`. The MEMW table supplies `old`. +fn memw_write(value: [BusValue; 8], base_lo: BusValue, base_hi: BusValue) -> Vec { + let mut v = Vec::with_capacity(16); + v.push(BusValue::constant(0)); // is_register = 0 (memory) + v.push(base_lo); + v.push(base_hi); + v.extend(value); + v.push(packed(cols::TIMESTAMP_0)); // ts_lo + v.push(packed(cols::TIMESTAMP_1)); // ts_hi + v.push(BusValue::constant(0)); // w2 + v.push(BusValue::constant(0)); // w4 + v.push(BusValue::constant(1)); // w8 = 1 (8-byte write) + v +} + +/// Bus interactions: +/// - **`Ecall` receiver** (mult `mu`): `[timestamp, cast(HINT_SYSCALL_NUMBER, +/// DWordWL)]` — HALT-shaped, balances the CPU's ECALL send. +/// - **MEMW write senders** (mult `mu`, ×4): the four 8-byte writes of the output +/// at `out_addr` +0/8/16/24, timestamp `T`. Received by the MEMW table. +pub fn bus_interactions() -> Vec { + let mu = || Multiplicity::Column(cols::MU); + let mut out = Vec::with_capacity(5); + + // ECALL receiver: [ts_lo, ts_hi, syscall_lo32, syscall_hi32]. + out.push(BusInteraction::receiver( + BusId::Ecall, + mu(), + vec![ + packed(cols::TIMESTAMP_0), + packed(cols::TIMESTAMP_1), + BusValue::constant(HINT_SYSCALL_NUMBER & 0xFFFF_FFFF), + BusValue::constant(HINT_SYSCALL_NUMBER >> 32), + ], + )); + + // write output: 4 doublewords at out_addr + 8i (timestamp T). + for i in 0..4 { + let base_lo = BusValue::linear(vec![ + LinearTerm::Column { + coefficient: 1, + column: cols::ADDR_OUT_0, + }, + LinearTerm::Constant((8 * i) as i64), + ]); + out.push(BusInteraction::sender( + BusId::Memw, + mu(), + memw_write(out_dword_bytes(i), base_lo, packed(cols::ADDR_OUT_1)), + )); + } + + out +} diff --git a/prover/src/tables/mod.rs b/prover/src/tables/mod.rs index 0a86e4149..f1a899f56 100644 --- a/prover/src/tables/mod.rs +++ b/prover/src/tables/mod.rs @@ -34,6 +34,7 @@ pub mod ecsm; pub mod eq; pub mod global_memory; pub mod halt; +pub mod hint; pub mod keccak; pub mod keccak_rc; pub mod keccak_rnd; diff --git a/prover/src/tables/trace_builder.rs b/prover/src/tables/trace_builder.rs index ee68f0be9..76b08b68b 100644 --- a/prover/src/tables/trace_builder.rs +++ b/prover/src/tables/trace_builder.rs @@ -51,6 +51,7 @@ use super::ecdas; use super::ecsm; use super::eq; use super::halt; +use super::hint; use super::keccak::{self, KeccakOperation}; use super::keccak_rc; use super::keccak_rnd::{self, KeccakRoundOperation}; @@ -549,6 +550,7 @@ fn collect_ops_from_cpu( Vec, Vec, Vec, + Vec, ) { let mut memw = MemwBuckets::with_register_capacity(cpu_ops.len() * 3); let mut load_ops = Vec::with_capacity(cpu_ops.len() / 8 + 1); @@ -560,6 +562,7 @@ fn collect_ops_from_cpu( let mut cpu32_ops = Vec::new(); let mut ecsm_ops = Vec::new(); let mut ecdas_ops = Vec::new(); + let mut hint_ops = Vec::new(); // Seed from the carried x254 (0 for a monolithic run or the first epoch) so a // continuation epoch indexes its commits globally, matching the x254 the // register binding transports across epochs. Resetting to 0 here would drift @@ -654,6 +657,13 @@ fn collect_ops_from_cpu( ecdas_ops.extend(ecdas_rows); } + // Collect Hint ecall operations (the 32-byte output write). BENCH ONLY. + if op.ecall_hint { + let (hint_memw, hint_op) = collect_hint_ops(op, memory_state, register_state); + memw.extend_ops(hint_memw); + hint_ops.push(hint_op); + } + // --- ALU chip dispatch (no state tracking) --- // Word (`*W`) instructions are delegated to CPU32 (which itself drives // the ALU chips); the main CPU does not send the ALU bus for them, so we @@ -709,6 +719,7 @@ fn collect_ops_from_cpu( cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, ) } @@ -948,6 +959,63 @@ fn collect_ecsm_ops( (memw_ops, ecsm_op, ecdas_ops) } +/// Collects the memory operations for a `Hint` ecall (BENCH ONLY). +/// +/// The `hint` ecall writes a 32-byte value (a modular inverse / sqrt) to guest +/// memory *directly* — bypassing the CPU load/store decode — so the trace builder +/// must reproduce that write itself: the value is not carried in the CPU log. We +/// re-derive the operand addresses from the register state (a0/a1/a2 = x10/x11/x12, +/// like ECSM), read the input from the replayed memory, recompute the output with +/// the executor's `compute_hint` (deterministic, same k256 arithmetic), then emit +/// four 8-byte MEMW writes at `out_addr` +0/8/16/24 and advance `memory_state`. +/// +/// The input read is intentionally not modeled (a read leaves the value unchanged; +/// the guest supplied the input via ordinary stores). The value itself is +/// unconstrained — soundness lives in the guest's in-circuit verify. +fn collect_hint_ops( + op: &CpuOperation, + memory_state: &mut MemoryState, + register_state: &mut RegisterState, +) -> (Vec, hint::HintOperation) { + let t = op.timestamp; + let hint_id = register_state.read(10).0; + let in_addr = register_state.read(11).0; + let out_addr = register_state.read(12).0; + + // Read the 32-byte big-endian input from the replayed memory. + let mut input = [0u8; 32]; + for (i, b) in input.iter_mut().enumerate() { + *b = memory_state.read_byte(in_addr.wrapping_add(i as u64)).0; + } + + // Recompute the output exactly as the executor did (the value isn't in the log). + let out_bytes = executor::vm::instruction::execution::compute_hint(hint_id, &input); + + // Emit the 32-byte output as four 8-byte MEMW writes at ts = T. + let mut memw_ops = Vec::with_capacity(4); + for i in 0..4 { + let addr = out_addr.wrapping_add((8 * i) as u64); + let mut value = [0u32; 8]; + let mut dword = 0u64; + for j in 0..8 { + let byte = out_bytes[8 * i + j]; + value[j] = byte as u32; + dword |= (byte as u64) << (8 * j); + } + let (old_vals, old_ts) = memory_state.read_bytes(addr, 8); + memw_ops + .push(MemwOperation::new(false, addr, value, t, 8, false).with_old(old_vals, old_ts)); + memory_state.write_bytes(addr, dword, 8, t); + } + + let hint_op = hint::HintOperation { + timestamp: t, + out_addr, + out_bytes, + }; + (memw_ops, hint_op) +} + /// Collects register read/write operations (M1, M3, M5) from CpuOperation, /// pushing them into `memw_ops`. fn collect_register_ops_from_cpu( @@ -2714,6 +2782,9 @@ pub struct Traces { /// ECDAS double/add table (variable rows per ecall) pub ecdas: TraceTable, + /// HINT table (one row per non-constraining hint ecall). BENCH ONLY. + pub hint: TraceTable, + /// MEMW_R register-only fast-path traces (split into chunks of max_rows::MEMW_R) pub memw_registers: Vec>, /// Local-to-global boundary table for continuation epochs. Empty unless the @@ -2756,6 +2827,8 @@ struct CollectedOps { // EC scalar-multiplication accelerator chips. ecsm_ops: Vec, ecdas_ops: Vec, + // Non-constraining hint ecall (BENCH ONLY). + hint_ops: Vec, } /// Chunk raw ops and generate one trace table per chunk. When `storage_mode` @@ -2810,6 +2883,7 @@ fn collect_all_ops( cpu32_ops: Vec, ecsm_ops: Vec, ecdas_ops: Vec, + hint_ops: Vec, register_state: &mut RegisterState, is_final: bool, ) -> CollectedOps { @@ -2952,6 +3026,7 @@ fn collect_all_ops( cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, } } @@ -2995,6 +3070,7 @@ fn build_traces( cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, } = ops; // ===================================================================== @@ -3356,6 +3432,8 @@ fn build_traces( // ECSM accelerator traces (empty/all-padding for programs that do not use ECSM). let gen_ecsm = || ecsm::generate_ecsm_trace(&ecsm_ops); let gen_ecdas = || ecdas::generate_ecdas_trace(&ecdas_ops); + // HINT table (all-padding for programs that make no hint ecalls). BENCH ONLY. + let gen_hint = || hint::generate_hint_trace(&hint_ops); let (mut cpus_slot, mut memws_slot, mut memw_aligneds_slot, mut memw_registers_slot) = (None, None, None, None); @@ -3368,6 +3446,7 @@ fn build_traces( let (mut eqs_slot, mut bytewises_slot, mut stores_slot, mut cpu32s_slot) = (None, None, None, None); let (mut ecsm_slot, mut ecdas_slot) = (None, None); + let mut hint_slot = None; #[cfg(feature = "disk-spill")] let sequential = storage_mode == StorageMode::Disk || cfg!(not(feature = "parallel")); @@ -3409,6 +3488,7 @@ fn build_traces( spawn_into!(cpu32s_slot, gen_cpu32s); spawn_into!(ecsm_slot, gen_ecsm); spawn_into!(ecdas_slot, gen_ecdas); + spawn_into!(hint_slot, gen_hint); }); } else { cpus_slot = Some(gen_cpus()); @@ -3436,6 +3516,7 @@ fn build_traces( cpu32s_slot = Some(gen_cpu32s()); ecsm_slot = Some(gen_ecsm()); ecdas_slot = Some(gen_ecdas()); + hint_slot = Some(gen_hint()); } const PHASE5_RAN: &str = "phase 5 generation ran in one of the branches above"; @@ -3470,6 +3551,7 @@ fn build_traces( let mut halt_trace = halt_slot.expect(PHASE5_RAN); let ecsm_trace = ecsm_slot.expect(PHASE5_RAN); let ecdas_trace = ecdas_slot.expect(PHASE5_RAN); + let hint_trace = hint_slot.expect(PHASE5_RAN); // Fixed-size and per-page tables aren't built through `chunk_and_generate`, // so spill them here before returning. @@ -3537,6 +3619,7 @@ fn build_traces( keccak_rc: keccak_rc_trace, ecsm: ecsm_trace, ecdas: ecdas_trace, + hint: hint_trace, memw_registers, local_to_global, touched_memory_cells, @@ -3799,6 +3882,7 @@ impl Traces { use super::ecsm::cols::NUM_COLUMNS as ECSM_COLS; use super::eq::cols::NUM_COLUMNS as EQ_COLS; use super::halt::cols::NUM_COLUMNS as HALT_COLS; + use super::hint::cols::NUM_COLUMNS as HINT_COLS; use super::keccak::cols::NUM_COLUMNS as KECCAK_COLS; use super::keccak_rc::NUM_PRECOMPUTED_COLS as KECCAK_RC_PRECOMPUTED; use super::keccak_rc::cols::NUM_COLUMNS as KECCAK_RC_COLS; @@ -3837,6 +3921,7 @@ impl Traces { keccak_rc, ecsm, ecdas, + hint, memw_registers, eqs, bytewises, @@ -3904,6 +3989,7 @@ impl Traces { } total += (ecsm.num_rows() * ECSM_COLS) as u64; total += (ecdas.num_rows() * ECDAS_COLS) as u64; + total += (hint.num_rows() * HINT_COLS) as u64; total } @@ -3945,6 +4031,7 @@ impl Traces { let n_cpu32 = aux_cols(super::cpu32::bus_interactions().len()); let n_ecsm = aux_cols(super::ecsm::bus_interactions().len()); let n_ecdas = aux_cols(super::ecdas::bus_interactions().len()); + let n_hint = aux_cols(super::hint::bus_interactions().len()); let Traces { cpus, @@ -3967,6 +4054,7 @@ impl Traces { keccak_rc, ecsm, ecdas, + hint, memw_registers, eqs, bytewises, @@ -4034,6 +4122,7 @@ impl Traces { } total += (ecsm.num_rows() * n_ecsm) as u64; total += (ecdas.num_rows() * n_ecdas) as u64; + total += (hint.num_rows() * n_hint) as u64; total } @@ -4256,6 +4345,7 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, ) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); #[cfg(feature = "instruments")] drop(__sp); @@ -4274,6 +4364,7 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, &mut register_state, is_final, ); @@ -4333,6 +4424,7 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, ) = collect_ops_from_cpu(&cpu_ops, &mut memory_state, &mut register_state); let ops = collect_all_ops( @@ -4347,6 +4439,7 @@ impl Traces { cpu32_ops, ecsm_ops, ecdas_ops, + hint_ops, &mut register_state, true, ); diff --git a/prover/src/test_utils.rs b/prover/src/test_utils.rs index 6dd28ce71..005a22abb 100644 --- a/prover/src/test_utils.rs +++ b/prover/src/test_utils.rs @@ -65,6 +65,7 @@ use crate::tables::ecsm::{ }; use crate::tables::eq::{EqConstraints, bus_interactions as eq_bus_interactions, cols as eq_cols}; use crate::tables::halt::{bus_interactions as halt_bus_interactions, cols as halt_cols}; +use crate::tables::hint::{bus_interactions as hint_bus_interactions, cols as hint_cols}; use crate::tables::keccak::{ KeccakConstraints, bus_interactions as keccak_bus_interactions, cols as keccak_cols, }; @@ -840,6 +841,19 @@ pub fn create_halt_air(proof_options: &ProofOptions) -> ConcreteVmAir ConcreteVmAir { + build_air( + hint_cols::NUM_COLUMNS, + hint_bus_interactions(), + proof_options, + 1, + EmptyConstraints, + "HINT", + ) +} + /// Create COMMIT AIR with constraints and bus interactions. pub fn create_commit_air(proof_options: &ProofOptions) -> ConcreteVmAir { build_air( diff --git a/prover/src/tests/prove_elfs_tests.rs b/prover/src/tests/prove_elfs_tests.rs index ffe9071b2..b02b65e81 100644 --- a/prover/src/tests/prove_elfs_tests.rs +++ b/prover/src/tests/prove_elfs_tests.rs @@ -1210,6 +1210,115 @@ fn test_prove_ecsm_rust_guest() { ); } +/// P0 for the non-constraining `Hint` ecall (BENCH ONLY): the minimal Rust guest +/// does one `hint` call (secp256k1 base-field inverse of 3) and commits the result. +/// This exercises exactly the HINT table's bus surface (Ecall receive + the four +/// 8-byte output MEMW writes) end-to-end through prove→verify, de-risking the bus +/// balance before scaling to real consumers. The committed output must equal the +/// value the executor's `compute_hint` produced (= 3^{-1} mod p). +#[test] +fn test_prove_hint_min_rust_guest() { + let _ = env_logger::builder().is_test(true).try_init(); + + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_min.elf")) + .expect("hint_min.elf not found — run `make compile-programs-rust`"); + + let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + assert!( + verify_vm_minimal(&proof, &elf_bytes), + "hint_min rust guest should verify" + ); + + // Committed output must equal the hinted value (field inverse of 3, 32-byte BE). + let mut input = [0u8; 32]; + input[31] = 3; + let expected = + executor::vm::instruction::execution::compute_hint(0 /* HINT_FIELD_INV */, &input); + assert_eq!(proof.public_output, expected.to_vec()); +} + +/// Multi-hint P0/P2 (BENCH ONLY): three `hint` ecalls, each result read back with +/// ordinary `LOAD`s. Complements `test_prove_hint_min_rust_guest` by proving the +/// paths the ethrex consumer relies on that a single-call guest doesn't: **multiple +/// real HINT rows** (padded) and **read-back via normal LOAD** (MEMW reads chaining +/// to the HINT writes). Committed output = XOR of the three field inverses. +#[test] +fn test_prove_hint_multi_rust_guest() { + let _ = env_logger::builder().is_test(true).try_init(); + + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_multi.elf")) + .expect("hint_multi.elf not found — run `make compile-programs-rust`"); + + let proof = prove_vm_minimal(&elf_bytes, &[], &Default::default()); + assert!( + verify_vm_minimal(&proof, &elf_bytes), + "hint_multi rust guest should verify" + ); + + // Expected = XOR of field-inverses of 3, 5, 7 (32-byte BE), matching the guest. + let mut expected = [0u8; 32]; + for seed in [3u8, 5u8, 7u8] { + let mut input = [0u8; 32]; + input[31] = seed; + let inv = + executor::vm::instruction::execution::compute_hint(0 /* HINT_FIELD_INV */, &input); + for i in 0..32 { + expected[i] ^= inv[i]; + } + } + assert_eq!(proof.public_output, expected.to_vec()); +} + +/// Soundness (BENCH ONLY): the verifier REJECTS a forged hint output. +/// +/// The HINT table's `out_bytes` are unconstrained *by the table* — the point of a +/// non-constraining hint. They are pinned instead by the memory argument: the HINT +/// table *sends* the output as MEMW writes, and the MEMW table *receives* the honest +/// values `collect_hint_ops` derived (recomputed from the input, written into +/// `memory_state`). Forge one output byte on the (single) real HINT row and the MEMW +/// write it sends no longer matches the received write → the Memw LogUp bus unbalances +/// → the proof must fail to verify. This is what makes the hint value load-bearing +/// even though the guest here does no in-circuit verify. Mirrors the ECSM analog. +#[test] +fn test_prove_hint_min_forged_result_rejected() { + use crate::tables::hint::cols as hint_cols; + + let _ = env_logger::builder().is_test(true).try_init(); + + let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("workspace root") + .to_path_buf(); + let elf_bytes = + std::fs::read(workspace_root.join("executor/program_artifacts/rust/hint_min.elf")) + .expect("hint_min.elf not found — run `make compile-programs-rust`"); + let elf = Elf::load(&elf_bytes).expect("Failed to load ELF"); + let executor = Executor::new(&elf, vec![]).expect("Failed to create executor"); + let result = executor.run().expect("Failed to run program"); + let mut traces = + Traces::from_elf_and_logs_minimal(&elf, &result.logs, &Default::default(), &[]).unwrap(); + + // Forge the low byte of the output on the (single) real HINT row. + let orig = *traces.hint.main_table.get(0, hint_cols::out(0)); + let forged = orig + FieldElement::::one(); + traces.hint.main_table.set(0, hint_cols::out(0), forged); + + assert!( + !prove_and_verify_vm_minimal(&elf, &mut traces), + "Verifier must reject a forged hint output byte" + ); +} + /// Soundness: the verifier REJECTS a forged ECSM result. /// /// A malicious prover must not be able to claim a wrong `k·G`. We tamper the result diff --git a/syscalls/Cargo.lock b/syscalls/Cargo.lock index 34e481dd8..3d18e4724 100644 --- a/syscalls/Cargo.lock +++ b/syscalls/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "block-buffer" version = "0.10.4" @@ -64,6 +58,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dlmalloc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5208a115eaba24916f7456929832e310a81518c641f93fee4f89aa93aa3675" +dependencies = [ + "cfg-if", + "libc", + "windows-sys", +] + [[package]] name = "embedded-alloc" version = "0.6.0" @@ -128,6 +133,8 @@ dependencies = [ name = "lambda-vm-syscalls" version = "0.1.0" dependencies = [ + "critical-section", + "dlmalloc", "embedded-alloc", "getrandom 0.2.17", "getrandom 0.3.4", @@ -247,7 +254,7 @@ checksum = "7d323d13972c1b104aa036bc692cd08b822c8bbf23d79a27c526095856499799" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -258,15 +265,14 @@ checksum = "8188909339ccc0c68cfb5a04648313f09621e8b87dc03095454f1a11f6c5d436" [[package]] name = "rlsf" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1646a59a9734b8b7a0ac51689388a60fe1625d4b956348e9de07591a1478457a" +checksum = "07393724337be2ee43a9d86164df4505746874a3fa65913374bc6d6a92314362" dependencies = [ "cfg-if", "const-default", "libc", "rustversion", - "svgbobdoc", ] [[package]] @@ -285,30 +291,6 @@ dependencies = [ "keccak", ] -[[package]] -name = "svgbobdoc" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c04b93fc15d79b39c63218f15e3fdffaa4c227830686e3b7c5f41244eb3e50" -dependencies = [ - "base64", - "proc-macro2", - "quote", - "syn 1.0.109", - "unicode-width", -] - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.119" @@ -337,7 +319,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] [[package]] @@ -352,12 +334,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "version_check" version = "0.9.5" @@ -379,6 +355,21 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "wit-bindgen" version = "0.57.1" @@ -402,5 +393,5 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn", ] diff --git a/syscalls/Cargo.toml b/syscalls/Cargo.toml index 0460a2435..ecd9775c2 100644 --- a/syscalls/Cargo.toml +++ b/syscalls/Cargo.toml @@ -4,6 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] +# embedded-alloc TLSF heap: the previous default guest allocator, now selectable via +# the `tlsf-alloc` feature. Kept a hard dep so switching allocators needs no dep edits; +# it's only linked into the guest and dropped when dlmalloc (the default) is selected. embedded-alloc = "0.6" riscv = { version = "0.15", features = ["critical-section-single-hart"] } thiserror = "1.0" @@ -11,6 +14,20 @@ getrandom = { version = "0.3.4", default-features = false } getrandom_v2 = {version = "0.2.15", features = ["custom"], package = "getrandom"} lazy_static = "1.5.0" rand = "0.9.2" +# Default guest allocator: Doug Lea's malloc. Cheaper per alloc/free than TLSF on zkVM +# guests (fewer guest cycles + smaller memory table) and still reclaims freed memory +# (no OOM). `critical-section` gives the Sync a #[global_allocator] static needs; its +# single-hart impl comes from `riscv` above. See `src/allocator.rs`. +dlmalloc = { version = "0.2.14", default-features = false } +critical-section = "1.2" + +[features] +# BENCH/fallback guest allocators — the default (no feature) is dlmalloc. At most one +# may be enabled (a compile_error guards conflicts). See `src/allocator.rs`. +# tlsf-alloc: embedded-alloc TLSF (the previous default; reclaims, safe). +# bump-alloc: monotonic bump, no-op dealloc — fastest but OOM-prone (never frees). +tlsf-alloc = [] +bump-alloc = [] [dev-dependencies] keccak = "0.1" diff --git a/syscalls/src/allocator.rs b/syscalls/src/allocator.rs index 78b2933e5..97c20f057 100644 --- a/syscalls/src/allocator.rs +++ b/syscalls/src/allocator.rs @@ -1,23 +1,219 @@ -use embedded_alloc::TlsfHeap as Heap; use riscv as _; -// Only the guest routes Rust allocations through this heap; on host (e.g. -// `cargo test` for the sponge's differential tests) the attribute would hijack -// the test harness's allocator with a never-initialized heap and abort. -#[cfg_attr(target_arch = "riscv64", global_allocator)] -static HEAP: Heap = Heap::empty(); - const MAX_MEMORY_SIZE: usize = 0xC000_0000; const WORD_SIZE: usize = 4; -pub fn init_allocator() { - { - unsafe extern "C" { - static _end: u8; +// Guest global allocator, selectable at build time (at most one BENCH feature): +// - default: Doug Lea's malloc (dlmalloc), backed by a bump "system" provider that +// hands dlmalloc page-aligned chunks from the guest heap. dlmalloc does all +// sub-allocation churn itself, so — unlike a raw bump allocator — it reuses freed +// memory (no OOM) while executing fewer guest instructions per alloc/free than TLSF +// and cutting memory operations (smaller memory table = cheaper proof). +// - `tlsf-alloc` feature: embedded-alloc's TLSF heap — the previous default, kept a +// flag away for A/B or fallback (reclaims freed memory, safe for arbitrary churn). +// - `bump-alloc` feature (BENCH): a monotonic bump allocator that never frees (no-op +// `dealloc`) and skips zeroing in `alloc_zeroed` (guest memory is zero-initialized +// and bump never reuses a region, so fresh bytes read as 0). Fastest per-op but the +// footprint grows monotonically — OOM-prone (see the roadmap's §1.2 risk analysis). +// +// Only the guest installs a #[global_allocator]; on host (e.g. `cargo test` for the +// sponge's differential tests) the attribute would hijack the test harness's +// allocator with a never-initialized heap and abort. + +#[cfg(all(feature = "tlsf-alloc", feature = "bump-alloc"))] +compile_error!("`tlsf-alloc` and `bump-alloc` are mutually exclusive allocator features"); + +#[cfg(not(any(feature = "tlsf-alloc", feature = "bump-alloc")))] +mod imp { + use core::alloc::{GlobalAlloc, Layout}; + use core::cell::RefCell; + use core::sync::atomic::{AtomicUsize, Ordering}; + use critical_section::Mutex; + use dlmalloc::{Allocator, Dlmalloc}; + + // Page granularity dlmalloc requests memory in. Must be a power of two; the guest + // heap region is 3 GiB so the value only affects the segment rounding below. + const PAGE_SIZE: usize = 4096; + + // The "system" side of dlmalloc: instead of mmap/sbrk (absent on the guest) it + // bump-allocates page-aligned segments from the single contiguous heap region + // [_end, MAX_MEMORY_SIZE). It never releases a segment (`free`/`free_part`/ + // `remap` all decline) — dlmalloc itself owns all reuse of freed *user* + // allocations against this fixed backing store, which is what keeps churny + // workloads OOM-free unlike a raw bump allocator. + struct BumpSystem; + + // Single-hart guest → `Relaxed` atomics are contention-free. + static HEAP_POS: AtomicUsize = AtomicUsize::new(0); + static HEAP_END: AtomicUsize = AtomicUsize::new(0); + + unsafe impl Allocator for BumpSystem { + fn alloc(&self, size: usize) -> (*mut u8, usize, u32) { + // Round up to a page so consecutive segments stay page-aligned. + let size = size.wrapping_add(PAGE_SIZE - 1) & !(PAGE_SIZE - 1); + let pos = HEAP_POS.load(Ordering::Relaxed); + match pos.checked_add(size) { + Some(new_pos) if new_pos <= HEAP_END.load(Ordering::Relaxed) => { + HEAP_POS.store(new_pos, Ordering::Relaxed); + // flags = 0: a plain external segment (never partially released). + (pos as *mut u8, size, 0) + } + // Out of heap → null makes dlmalloc return null → handle_alloc_error. + _ => (core::ptr::null_mut(), 0, 0), + } } - let heap_pos: usize = unsafe { (&_end) as *const u8 as usize }; - unsafe { HEAP.init(heap_pos, MAX_MEMORY_SIZE - heap_pos) } + + fn remap(&self, _ptr: *mut u8, _old: usize, _new: usize, _can_move: bool) -> *mut u8 { + core::ptr::null_mut() + } + + fn free_part(&self, _ptr: *mut u8, _old: usize, _new: usize) -> bool { + false + } + + fn free(&self, _ptr: *mut u8, _size: usize) -> bool { + false + } + + fn can_release_part(&self, _flags: u32) -> bool { + false + } + + fn allocates_zeros(&self) -> bool { + // Guest memory is zero-initialized and this provider never reuses a + // segment, so system-fresh bytes read as 0 → dlmalloc's calloc skips the + // memset for system-fresh memory (it still zeroes recycled blocks itself). + true + } + + fn page_size(&self) -> usize { + PAGE_SIZE + } + } + + // Dlmalloc is Send but !Sync, so it can't sit in a static directly. A single-hart + // critical section serializes access and supplies the Sync a #[global_allocator] + // static requires — the same primitive embedded-alloc's TLSF heap uses. + static DLMALLOC: Mutex>> = + Mutex::new(RefCell::new(Dlmalloc::new_with_allocator(BumpSystem))); + + struct DlGlobal; + + #[cfg_attr(target_arch = "riscv64", global_allocator)] + static ALLOC: DlGlobal = DlGlobal; + + pub fn init(heap_start: usize, heap_end: usize) { + HEAP_POS.store(heap_start, Ordering::Relaxed); + HEAP_END.store(heap_end, Ordering::Relaxed); + } + + unsafe impl GlobalAlloc for DlGlobal { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + critical_section::with(|cs| unsafe { + DLMALLOC + .borrow(cs) + .borrow_mut() + .malloc(layout.size(), layout.align()) + }) + } + + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { + critical_section::with(|cs| unsafe { + DLMALLOC + .borrow(cs) + .borrow_mut() + .free(ptr, layout.size(), layout.align()) + }) + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + critical_section::with(|cs| unsafe { + DLMALLOC + .borrow(cs) + .borrow_mut() + .calloc(layout.size(), layout.align()) + }) + } + + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { + critical_section::with(|cs| unsafe { + DLMALLOC.borrow(cs).borrow_mut().realloc( + ptr, + layout.size(), + layout.align(), + new_size, + ) + }) + } + } +} + +#[cfg(feature = "tlsf-alloc")] +mod imp { + use embedded_alloc::TlsfHeap as Heap; + + #[cfg_attr(target_arch = "riscv64", global_allocator)] + static HEAP: Heap = Heap::empty(); + + pub fn init(heap_start: usize, heap_end: usize) { + unsafe { HEAP.init(heap_start, heap_end - heap_start) } + } +} + +#[cfg(feature = "bump-alloc")] +mod imp { + use core::alloc::{GlobalAlloc, Layout}; + use core::sync::atomic::{AtomicUsize, Ordering}; + + struct BumpAlloc; + + // Single-hart guest → `Relaxed` atomics are contention-free and avoid the + // `static mut` edition-2024 lints. + static HEAP_POS: AtomicUsize = AtomicUsize::new(0); + static HEAP_END: AtomicUsize = AtomicUsize::new(0); + + #[cfg_attr(target_arch = "riscv64", global_allocator)] + static ALLOC: BumpAlloc = BumpAlloc; + + pub fn init(heap_start: usize, heap_end: usize) { + HEAP_POS.store(heap_start, Ordering::Relaxed); + HEAP_END.store(heap_end, Ordering::Relaxed); + } + + unsafe impl GlobalAlloc for BumpAlloc { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + let align = layout.align(); + let pos = HEAP_POS.load(Ordering::Relaxed); + // `align` is a power of two per the Layout contract. + let aligned = pos.wrapping_add(align - 1) & !(align - 1); + match aligned.checked_add(layout.size()) { + Some(new_pos) if new_pos <= HEAP_END.load(Ordering::Relaxed) => { + HEAP_POS.store(new_pos, Ordering::Relaxed); + aligned as *mut u8 + } + // Out of heap → null makes the caller's `handle_alloc_error` abort. + _ => core::ptr::null_mut(), + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + // A bump allocator never reclaims. + } + + unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { + // Guest memory is zero-initialized and bump never reuses a freed region, + // so freshly bumped memory already reads as zero — skip the memset. + unsafe { self.alloc(layout) } + } + } +} + +pub fn init_allocator() { + unsafe extern "C" { + static _end: u8; } + let heap_pos: usize = unsafe { (&_end) as *const u8 as usize }; + imp::init(heap_pos, MAX_MEMORY_SIZE); } /// # Safety @@ -26,8 +222,8 @@ pub fn init_allocator() { /// It is only for rust std internal uses #[unsafe(no_mangle)] pub unsafe extern "C" fn sys_alloc_aligned(bytes: usize, align: usize) -> *mut u8 { - use core::alloc::GlobalAlloc; - unsafe { HEAP.alloc(core::alloc::Layout::from_size_align(bytes, align).unwrap()) } + // Route through whichever `#[global_allocator]` is installed (dlmalloc/TLSF/bump). + unsafe { std::alloc::alloc(core::alloc::Layout::from_size_align(bytes, align).unwrap()) } } /// # Safety diff --git a/syscalls/src/syscalls.rs b/syscalls/src/syscalls.rs index 7165dff81..32176f791 100644 --- a/syscalls/src/syscalls.rs +++ b/syscalls/src/syscalls.rs @@ -33,6 +33,16 @@ const KECCAK_SYSCALL_NUMBER: usize = usize::MAX - 1; #[cfg(target_arch = "riscv64")] const ECSM_SYSCALL_NUMBER: usize = usize::MAX - 10; +/// Syscall number for the non-constraining Hint ecall (BENCH ONLY). +/// Must match `executor::...::execution::HINT_SYSCALL_NUMBER` (u64::MAX - 20). +#[cfg(target_arch = "riscv64")] +const HINT_SYSCALL_NUMBER: usize = usize::MAX - 20; + +/// Hint selectors passed in `a0` (must match the executor's `HINT_*`). +pub const HINT_FIELD_INV: usize = 0; +pub const HINT_SCALAR_INV: usize = 1; +pub const HINT_FIELD_SQRT: usize = 2; + /// No-op. The `Print` ecall (a7=1) has no receiver on the Ecall bus, so emitting /// it makes the LogUp bus unbalance and the proof fail to verify. Printing isn't /// needed in provable programs, so `print_string` does nothing on every target. @@ -187,6 +197,31 @@ pub fn ecsm_mul(_xr: &mut [u8; 32], _xg: &[u8; 32], _k: &[u8; 32]) { unimplemented!("syscalls are only implemented for riscv64 targets"); } +/// BENCH ONLY. Ask the host for a non-constraining hint (modular inverse/sqrt). +/// `hint_id` selects the operation ([`HINT_FIELD_INV`]/[`HINT_SCALAR_INV`]/ +/// [`HINT_FIELD_SQRT`]); `input`/`out` are 32-byte **big-endian** field/scalar +/// elements — k256's own serialization, so consumers pass `to_bytes()` straight +/// through. Note this differs from [`ecsm_mul`], which is little-endian. +/// The result is UNVERIFIED — the caller MUST check it in-guest +/// (e.g. `x·inv == 1`), since this ecall adds no correctness constraint. +#[cfg(target_arch = "riscv64")] +pub fn hint(hint_id: usize, out: &mut [u8; 32], input: &[u8; 32]) { + unsafe { + asm!( + "ecall", + in("a0") hint_id, // x10 = hint selector + in("a1") input.as_ptr(), // x11 = input address (32-byte BE) + in("a2") out.as_mut_ptr(), // x12 = output address (32-byte BE) + in("a7") HINT_SYSCALL_NUMBER, + ) + } +} + +#[cfg(not(target_arch = "riscv64"))] +pub fn hint(_hint_id: usize, _out: &mut [u8; 32], _input: &[u8; 32]) { + unimplemented!("syscalls are only implemented for riscv64 targets"); +} + // ============================================================================= // Stub implementations for unsupported std functions // These functions are required by Rust's std zkvm module but are not supported diff --git a/tooling/ethrex-tests/Cargo.lock b/tooling/ethrex-tests/Cargo.lock index 26f991c7a..46b6e075d 100644 --- a/tooling/ethrex-tests/Cargo.lock +++ b/tooling/ethrex-tests/Cargo.lock @@ -874,6 +874,7 @@ name = "executor" version = "0.1.0" dependencies = [ "ecsm", + "k256", "rustc-demangle", "thiserror 1.0.69", ]