Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
- Bound deferred precompile STARK proofs to the generated precompile ACE relation digest ([#3344](https://github.com/0xMiden/miden-vm/pull/3344)).
- [BREAKING] Split Poseidon2 permutation rows out of `ChipletsAir` into `Poseidon2PermutationAir`, and updated the recursive verifier ACE registry for three AIRs ([#3345](https://github.com/0xMiden/miden-vm/pull/3345)).
- [BREAKING] Optimize periodic columns evaluation for fewer ACE gates ([#3347](https://github.com/0xMiden/miden-vm/pull/3347)).
- [BREAKING] Optimize periodic columns evaluation for fewer ACE gates ([#3347](https://github.com/0xMiden/miden-vm/pull/3347)).
- [BREAKING] Moved the secp256k1 GLV endomorphism scalar decomposition from the ECDSA verifier's MASM/advice ABI into the precompiles prover's addition-chain strategy: `ecdsa_k256_keccak::verify` logs a plain `u1*G + u2*Q` claim, and the deferred prover satisfies it with a GLV-decomposed chain, certified in-circuit ([#3426](https://github.com/0xMiden/miden-vm/pull/3426)).
- Split dense `MastForest` order helpers and package serialization tests into smaller modules, and routed dense forest finalization and static library setup through dedicated builder and library methods ([#3346](https://github.com/0xMiden/miden-vm/pull/3346)).
- [BREAKING] Renamed module and kernel metadata APIs from `ModuleInfo`/`Kernel` to `ModuleDescriptor`/`KernelDescriptor`, including matching module descriptor method names ([#3356](https://github.com/0xMiden/miden-vm/pull/3356)).
- Replaced panics in `OverflowTable::restore_context()`, `get_current_overflow_stack()`, and `get_current_overflow_stack_mut()` with proper `OperationError` returns ([#3370](https://github.com/0xMiden/miden-vm/pull/3370)).
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/lib/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
miden-processor.workspace = true
miden-utils-sync.workspace = true
miden-air = { workspace = true, optional = true }
miden-ace-codegen = { workspace = true, optional = true }

Check warning on line 49 in crates/lib/core/Cargo.toml

View workflow job for this annotation

GitHub Actions / cargo-shear on ubuntu-latest

shear/misplaced_optional_dependency

misplaced optional dependency `miden-ace-codegen` (remove the `optional` flag and move to `[dev-dependencies]`)
miden-constraint-compiler = { workspace = true, optional = true }

# External dependencies
Expand All @@ -59,6 +59,7 @@
miden-assembly = { workspace = true, features = ["testing"] }
miden-core-lib-codegen = { workspace = true, features = ["std"] }
miden-package-registry = { workspace = true, features = ["resolver"] }
miden-precompiles-prover = { workspace = true, features = ["std"] }
miden-processor = { workspace = true, features = ["testing"] }
miden-utils-testing.workspace = true
num = { workspace = true }
Expand Down
87 changes: 52 additions & 35 deletions crates/lib/core/asm/crypto/dsa/ecdsa_k256_keccak.masm
Original file line number Diff line number Diff line change
Expand Up @@ -98,40 +98,67 @@ pub proc verify
# => [PREHASH_U32[8], pubkey_ptr, ...] where PREHASH[0] = byte_reverse(KeccakDigest[7])

exec.prepare_verify_inputs
# => [U1, Q, U2, SIG_R, ...]
# => [U1, U2, SIG_R, pubkey_ptr, ...]

dupw.1
# => [Q, U1, Q, U2, SIG_R, ...]
movup.12
# => [pubkey_ptr, U1, U2, SIG_R, ...]
dup add.8
# => [pubkey_ptr+8, pubkey_ptr, U1, U2, SIG_R, ...]
exec.k1_base::load_mem_stream
# => [BY, pubkey_ptr+16, pubkey_ptr, U1, U2, SIG_R, ...]
movup.5
# => [pubkey_ptr, BY, pubkey_ptr+16, U1, U2, SIG_R, ...]
exec.k1_base::load_mem_stream
# => [BX, pubkey_ptr+8, BY, pubkey_ptr+16, U1, U2, SIG_R, ...]
movup.4 drop
# => [BX, BY, pubkey_ptr+16, U1, U2, SIG_R, ...]
movup.8 drop
# => [BX, BY, U1, U2, SIG_R, ...]
exec.secp256k1::load_digest_pair
# => [Q, U1, U2, SIG_R, ...]
movdnw.2
# => [U1, U2, Q, SIG_R, ...]
exec.verify_point_2base
# => [VERIFY_POINT, SIG_R, ...]
exec.assert_x_eq_scalar_k1
# => [...]
end

# INTERNAL PROCEDURES — ECDSA
# ================================================================================================

#! Computes `R = u1*G + u2*Q` as a plain 2-base joint wNAF MSM, folding the degenerate `Q == G`
#! case into a single `(u1+u2)*G` multiply, the one collision a 2-base MSM has.
#!
#! Input: [U1, U2, Q, ...]
#! Output: [VERIFY_POINT, ...]
proc verify_point_2base
dupw.2
# => [Q, U1, U2, Q, ...]
exec.secp256k1::push_generator
# => [G, Q, U1, Q, U2, SIG_R, ...]
# => [G, Q, U1, U2, Q, ...]
exec.word::eq
# => [is_q_generator, U1, Q, U2, SIG_R, ...]
# => [is_q_generator, U1, U2, Q, ...]
if.true
movupw.2 swapw
# => [U1, U2, Q, SIG_R, ...]
exec.k1_scalar::add
# => [U1_PLUS_U2, Q, SIG_R, ...]
# => [U1_PLUS_U2, Q, ...]
swapw dropw
# => [U1_PLUS_U2, SIG_R, ...]
# => [U1_PLUS_U2, ...]
exec.secp256k1::mul_scalar_generator
else
exec.secp256k1::push_generator
# => [G, U1, Q, U2, SIG_R, ...]
exec.secp256k1::msm2
exec.secp256k1::msm2_generator
end
# => [VERIFY_POINT, SIG_R, ...]
exec.assert_x_eq_scalar_k1
# => [...]
# => [VERIFY_POINT, ...]
end

# INTERNAL PROCEDURES — ECDSA
# ================================================================================================

#! Computes ECDSA scalars U1 = z/s and U2 = r/s, keeping SIG_R for the final check.
#!
#! Input: [Z_U32[8], pubkey_ptr, ...]
#! Advice: [SIG_R[8] | SIG_S[8] | ...]
#! Output: [U1_DIGEST, Q_DIGEST, U2_DIGEST, SIG_R_DIGEST, ...]
#! Output: [U1_DIGEST, U2_DIGEST, SIG_R_DIGEST, pubkey_ptr, ...]
#!
#! `pubkey_ptr` rides through unread: the public key is loaded once, by the verification-point
#! procedure that also needs `Q`'s raw coordinate digests.
#!
#! Preconditions/provenance:
#! - Z_U32[8] is the Keccak256 message digest converted to little-endian scalar limbs;
Expand All @@ -141,36 +168,26 @@ proc prepare_verify_inputs
exec.k1_scalar::load_reduced_256
# => [Z, pubkey_ptr, ...]

movup.4
# => [pubkey_ptr, Z, ...]
exec.secp256k1::load_mem
# => [Q, Z, ...]
swapw
# => [Z, Q, ...]

exec.load_signature_k1
# => [SIG_S, SIG_R, Z, Q, ...]
# => [SIG_S, SIG_R, Z, pubkey_ptr, ...]

exec.k1_scalar::inv
# => [S_INV, SIG_R, Z, Q, ...]
# => [S_INV, SIG_R, Z, pubkey_ptr, ...]

dupw
movupw.3
exec.k1_scalar::mul
# => [U1 = Z*S_INV, S_INV, SIG_R, Q, ...]
# => [U1 = Z*S_INV, S_INV, SIG_R, pubkey_ptr, ...]

dupw.1
dupw.3
exec.k1_scalar::mul
# => [U2 = SIG_R*S_INV, U1, S_INV, SIG_R, Q, ...]
# => [U2 = SIG_R*S_INV, U1, S_INV, SIG_R, pubkey_ptr, ...]

swapw.2
# => [S_INV, U1, U2, SIG_R, Q, ...]
# => [S_INV, U1, U2, SIG_R, pubkey_ptr, ...]
dropw
# => [U1, U2, SIG_R, Q, ...]

movupw.3 swapw
# => [U1, Q, U2, SIG_R, ...]
# => [U1, U2, SIG_R, pubkey_ptr, ...]
end

#! Loads SIG_R and SIG_S from advice as k1 scalar VALUE digests, checks SIG_R != 0,
Expand Down
36 changes: 36 additions & 0 deletions crates/lib/core/codegen/src/masm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,40 @@ fn constant(value: Limbs, domain: UintDomain) -> ConstantMasm {
}
}

/// Renders `curve.extra_points()` (identity/generator's siblings, e.g. a GLV endomorphism image)
/// as `NAME_DIGEST` constant declarations. [`CurveId::extra_points`] is the single source of
/// truth: [`CurvePrecompile::init`] seeds the same points into the deferred-DAG init node set, so
/// the MASM constant and the runtime registration cannot drift apart.
fn render_curve_extra_constants(curve: CurveId) -> String {
let points = curve.extra_points().into_iter().map(|(name, point)| {
let digest = CurvePrecompile::value_node(curve, point).digest();
format!("const {name}_DIGEST = {}\n", word_literal(digest_word(digest)))
});
let base_constants = curve.extra_base_constants().into_iter().map(|(name, limbs)| {
let digest = UintPrecompile::value_node(curve.base_domain(), limbs).digest();
format!("const {name}_DIGEST = {}\n", word_literal(digest_word(digest)))
});
points.chain(base_constants).collect()
}

/// Renders `curve.extra_points()` and [`CurveId::extra_base_constants`] as `push_name` wrapper
/// procs (name lowercased), mirroring `GENERATOR_DIGEST`/`push_generator`.
fn render_curve_extra_procs(curve: CurveId) -> String {
let points = curve.extra_points().into_iter().map(|(name, _)| {
format!(
"\n#! Pushes the registered digest of the precomputed `{name}` point constant.\npub proc push_{proc_name}\n push.{name}_DIGEST\nend\n",
proc_name = name.to_lowercase(),
)
});
let base_constants = curve.extra_base_constants().into_iter().map(|(name, _)| {
format!(
"\n#! Pushes the canonical VALUE digest of the base-field constant `{name}`.\npub proc push_{proc_name}\n push.{name}_DIGEST\nend\n",
proc_name = name.to_lowercase(),
)
});
points.chain(base_constants).collect()
}

fn render_curve(config: &CurveMasmConfig) -> Result<String, String> {
let curve = config.curve;
let op_tag = |op_id| word_literal(tag_word(CurvePrecompile::op_tag(op_id)));
Expand Down Expand Up @@ -176,6 +210,8 @@ fn render_curve(config: &CurveMasmConfig) -> Result<String, String> {
"GENERATOR_DIGEST",
word_literal(digest_word(CurvePrecompile::generator_node(curve).digest())),
),
("EXTRA_CONSTANTS", render_curve_extra_constants(curve)),
("EXTRA_PROCS", render_curve_extra_procs(curve)),
];

render_template(CURVE_TEMPLATE, &replacements)
Expand Down
2 changes: 2 additions & 0 deletions crates/lib/core/codegen/src/templates/curve.masm.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const MSM_TAG = {{MSM_TAG}}
# Registered digests for CurvePrecompile init constants.
const IDENTITY_DIGEST = {{IDENTITY_DIGEST}}
const GENERATOR_DIGEST = {{GENERATOR_DIGEST}}
{{EXTRA_CONSTANTS}}

#! Constructs an affine curve VALUE node from two coordinate digests.
#! Input: [X_DIGEST, Y_DIGEST, ...]
Expand Down Expand Up @@ -131,6 +132,7 @@ end
pub proc push_generator
push.GENERATOR_DIGEST
end
{{EXTRA_PROCS}}


#! Registers `lhs + rhs` and returns the result expression digest.
Expand Down
10 changes: 5 additions & 5 deletions crates/lib/core/src/dsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,10 @@ pub mod ecdsa_k256_keccak {
/// by `ecdsa_k256_keccak::verify`.
///
/// The encoding is the structural order consumed from the advice stack:
/// `[QX[8] || QY[8] || SIG_R[8] || SIG_S[8]]`, where each value is a little-endian `u32` limb
/// represented as a field element. This preserves `r` and `s` exactly, omits the recovery ID,
/// and does not normalize or enforce low-s. The result is advice witness data, not a commitment
/// to the supplied signature encoding.
/// `[QX[8] || QY[8] || SIG_R[8] || SIG_S[8]]`, where each scalar value is a little-endian
/// `u32` limb represented as a field element. The signature portion preserves `r` and `s`
/// exactly, omits the recovery ID, and does not normalize or enforce low-s. The result is
/// advice witness data, not a commitment to the supplied signature encoding.
///
/// The public-key elements come from [`SequentialCommit::to_elements()`], matching the
/// commitment returned by [`public_key_commitment()`].
Expand All @@ -62,7 +62,7 @@ pub mod ecdsa_k256_keccak {
"ECDSA public key elements must be QX[8] || QY[8] native limbs",
);

let mut out = Vec::with_capacity(32);
let mut out = Vec::with_capacity(16 + 16);
out.extend(pk_elements);
out.extend_from_slice(&signature_felts(sig));
out
Expand Down
77 changes: 61 additions & 16 deletions crates/lib/core/tests/crypto/dsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,16 @@ use miden_crypto::{
SequentialCommit,
dsa::ecdsa_k256_keccak::{PublicKey, Signature, SigningKey},
};
use miden_precompiles::K1Scalar;
use miden_precompiles::{K1Scalar, SECP256K1_LAMBDA, scalar_mul_mod_n};
use miden_precompiles_prover::{HashFunction, prove_deferred_state, verify_deferred};
use miden_processor::{
DefaultHost, ExecutionError, ExecutionOptions, ExecutionOutput, FastProcessor, StackInputs,
advice::{AdviceInputs, AdviceStack},
};
use miden_utils_testing::crypto::Poseidon2;
use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng};

const VERIFY_EXPECTED_CYCLES: u64 = 1_453;
const VERIFY_EXPECTED_CYCLES: u64 = 1_465;
const VERIFY_EXPECTED_WIRE_ENTRIES: usize = 36;
const VERIFY_EXPECTED_WIRE_BYTES: usize = 2_455;

Expand All @@ -35,12 +36,61 @@ fn core_ecdsa_k256_keccak_verify_accepts_valid_signature() {
assert_eq!(wire.to_bytes().len(), VERIFY_EXPECTED_WIRE_BYTES);
}

/// Full round trip through the real precompile side prover: `verify` logs a plain 2-base MSM
/// claim (`R = u1*G + u2*Q`), and the side prover satisfies it with a GLV-decomposed addition
/// chain internally (`intro_endo`'s in-circuit `phi(G)`/`phi(Q)` certs, no untrusted advice) --
/// this proves those claims the deferred state above only checked structurally, then verifies the
/// resulting STARK proof against the same root the main VM committed.
#[test]
fn core_ecdsa_k256_keccak_verify_accepts_generator_public_key() {
let fixture = generator_public_key_fixture();
fn core_ecdsa_k256_keccak_verify_glv_claim_proves_and_verifies() {
let fixture = valid_fixture();
let output = run_verify(&fixture).expect("valid core ECDSA K256/Keccak signature must verify");

let output = run_verify(&fixture).expect("generator public key must verify");
assert_deferred_state_round_trips(&output);
let proof = prove_deferred_state(&output.deferred_state, HashFunction::Blake3_256)
.expect("the GLV-decomposed deferred claims must be provable");
let verified_root =
verify_deferred(&proof).expect("the GLV-decomposed deferred proof must verify");
assert_eq!(
verified_root,
output.deferred_state.root(),
"verified root must match the root the main VM committed",
);
}

/// A public key whose x-coordinate is `G_x`, `beta*G_x`, or `beta^2*G_x` puts it on the secp256k1
/// GLV endomorphism's orbit of the generator -- `Q` coincides with `G`, `phi(G)`, or `phi^2(G)` as
/// a point value. `verify` logs the same plain `u1*G + u2*Q` claim regardless; on the prover side,
/// `intro_endo`'s in-circuit value relation means a coincidence like this is ordinary point-store
/// dedup, not a forgery surface, so it needs no special-casing -- each such key must verify (and
/// prove) like any other.
///
/// Their discrete logs are `1`, `lambda`, `lambda^2` and the negations thereof, so no such key has
/// practical use. They are valid curve points all the same, and a verifier that traps on a valid
/// key decides it by accident rather than on the signature.
#[test]
fn core_ecdsa_k256_keccak_verify_accepts_glv_base_repeating_public_keys() {
let one = core::array::from_fn(|i| u32::from(i == 0));
let lambda_squared = scalar_mul_mod_n(SECP256K1_LAMBDA, SECP256K1_LAMBDA);

for (name, secret_scalar) in [
("Q == G", one),
("Q == -G", negate_scalar_mod_n(one)),
("Q == phi(G)", SECP256K1_LAMBDA),
("Q == phi^2(G)", lambda_squared),
] {
let sk = SigningKey::read_from_bytes(&le_limbs_to_be_bytes(secret_scalar))
.unwrap_or_else(|_| panic!("{name}: the secret scalar must be a valid key"));
let fixture = fixture_from_signing_key(sk);

let output = run_verify(&fixture)
.unwrap_or_else(|e| panic!("{name} must verify through the 2-base fallback: {e}"));

let proof = prove_deferred_state(&output.deferred_state, HashFunction::Blake3_256)
.unwrap_or_else(|_| panic!("{name}: the fallback's deferred claims must be provable"));
let verified_root = verify_deferred(&proof)
.unwrap_or_else(|_| panic!("{name}: the fallback's deferred proof must verify"));
assert_eq!(verified_root, output.deferred_state.root(), "{name}");
}
}

#[test]
Expand All @@ -63,7 +113,7 @@ fn core_ecdsa_k256_keccak_verify_accepts_high_s_untrusted_witness() {
"miden-crypto Rust verification must reject high-s",
);

set_s(&mut fixture, high_s);
fixture.advice = ecdsa_k256_keccak::encode_signature(&fixture.public_key, &high_s_signature);
run_verify(&fixture)
.expect("high-s remains an equivalent witness when signature advice is not committed");
}
Expand Down Expand Up @@ -170,17 +220,12 @@ struct Fixture {
}

fn valid_fixture() -> Fixture {
let mut rng = ChaCha20Rng::from_seed([0xe5; 32]);
let sk = SigningKey::with_rng(&mut rng);
fixture_from_signing_key(sk)
fixture_from_signing_key(default_signing_key())
}

fn generator_public_key_fixture() -> Fixture {
let mut secret_key_bytes = [0u8; 32];
secret_key_bytes[31] = 1;
let sk = SigningKey::read_from_bytes(&secret_key_bytes).expect("scalar 1 is a valid key");

fixture_from_signing_key(sk)
fn default_signing_key() -> SigningKey {
let mut rng = ChaCha20Rng::from_seed([0xe5; 32]);
SigningKey::with_rng(&mut rng)
}

fn fixture_from_signing_key(sk: SigningKey) -> Fixture {
Expand Down
Loading
Loading