Skip to content
Merged
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
8 changes: 6 additions & 2 deletions circuits/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,9 @@ fn create_test_input_notes_information(

let mut input_notes_info = Vec::new();
for note in notes.into_iter() {
let note_sk = sender_sk.gen_note_sk(note.stealth_address());
let note_sk = sender_sk
.gen_note_sk(note.stealth_address())
.expect("sender_sk should own the note");
let merkle_opening = tree
.opening(*note.pos())
.expect("There should be a note at the given position");
Expand All @@ -284,7 +286,9 @@ fn create_test_input_notes_information(
let value_blinder = note
.value_blinder(Some(&sender_vk))
.expect("sender_sk should own the note");
let nullifier = note.gen_nullifier(&sender_sk);
let nullifier = note
.gen_nullifier(sender_sk)
.expect("sender_sk should own the note");
let signature = note_sk.sign_double(rng, payload_hash);
input_notes_info.push(InputNoteInfo {
merkle_opening,
Expand Down
10 changes: 8 additions & 2 deletions circuits/tests/torsion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ fn transaction<const I: usize>(
if shifted == Shifted::NotePk {
let input = &mut inputs[I - 1];
let stealth_address = *input.note.stealth_address();
let note_sk = TP.sender_sk.gen_note_sk(&stealth_address);
let note_sk = TP
.sender_sk
.gen_note_sk(&stealth_address)
.expect("the sender owns the note");
let note_pk = stealth_address.note_pk().as_ref() + offset;
let stealth_address = StealthAddress::from_raw_unchecked(
*stealth_address.R(),
Expand All @@ -224,7 +227,10 @@ fn transaction<const I: usize>(
}
if shifted == Shifted::NotePkP {
let input = &mut inputs[0];
let note_sk = TP.sender_sk.gen_note_sk(input.note.stealth_address());
let note_sk = TP
.sender_sk
.gen_note_sk(input.note.stealth_address())
.expect("the sender owns the note");
let note_pk = *input.note.stealth_address().note_pk().as_ref();
let note_pk_p = JubJubExtended::from(input.note_pk_p) + offset;

Expand Down
10 changes: 10 additions & 0 deletions core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Speed up `ViewKey::owns` and `SecretKey::owns` [#319]
- Return an `Option` from `SecretKey::gen_note_sk` and `Note::gen_nullifier`,
`None` when the `R` of the stealth address is the identity or of small order
[#317]
- Redact secret scalar material from `SecretKey` and `ViewKey` debug output
[#316]
- Update `jubjub-schnorr` to v0.7.0 [#311]
Expand Down Expand Up @@ -46,6 +50,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Ignore a small-order component of `R` in `ViewKey::owns`, `SecretKey::owns`
and `SecretKey::gen_note_sk` [#317]
- Reject a stealth address whose `R` is the identity or of small order in
`ViewKey::owns` and `SecretKey::owns` [#317]
- Reject `PublicKey` encodings whose `A` or `B` is the identity [#312]
- Reject recovered note openings that do not match their value commitment
[#290]
Expand Down Expand Up @@ -491,6 +499,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Canonical implementation shielded by feature.

<!-- ISSUES -->
[#319]: https://github.com/dusk-network/phoenix/issues/319
[#317]: https://github.com/dusk-network/phoenix/issues/317
[#316]: https://github.com/dusk-network/phoenix/issues/316
[#285]: https://github.com/dusk-network/phoenix/issues/285
[#312]: https://github.com/dusk-network/phoenix/issues/312
Expand Down
23 changes: 21 additions & 2 deletions core/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
//
// Copyright (c) DUSK NETWORK. All rights reserved.

use dusk_jubjub::{JubJubAffine, JubJubExtended, JubJubScalar};
use dusk_jubjub::{JubJubAffine, JubJubExtended, JubJubScalar, dhke};

mod generator;
pub mod public;
pub mod secret;
pub mod view;
Expand All @@ -16,5 +17,23 @@ pub mod view;
/// Uses the legacy construction without a domain separator, which every
/// existing note key is derived with.
pub fn hash(p: &JubJubExtended) -> JubJubScalar {
JubJubScalar::hash_to_scalar(None, &JubJubAffine::from(p).to_bytes())
hash_affine(&JubJubAffine::from(p))
}

fn hash_affine(p: &JubJubAffine) -> JubJubScalar {
JubJubScalar::hash_to_scalar(None, &p.to_bytes())
}

/// Hashes the shared secret `a · R` of the secret `a` and the `R` of a
/// stealth address, as [`hash`] does.
///
/// The shared secret is derived with [`dhke`], which clears the cofactor of
/// `R`: a small-order component of `R` does not reach the result, and an
/// identity or small-order `R` yields none. For an `R` of prime order the
/// result is `H(a · R)`.
pub(crate) fn hash_shared_secret(
a: &JubJubScalar,
R: &JubJubExtended,
) -> Option<JubJubScalar> {
Option::from(dhke(a, R)).map(|aR| hash_affine(&aR))
}
128 changes: 128 additions & 0 deletions core/src/keys/generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

//! Multiplication of the generator with a table of its multiples.

use dusk_jubjub::{JubJubAffine, JubJubExtended, JubJubScalar};
use subtle::{Choice, ConditionallySelectable, ConstantTimeEq};

mod table;
use table::TABLE;

/// Computes `s · G` for the generator `G`, the same point as
/// `GENERATOR_EXTENDED * s`, in constant time.
Comment on lines +15 to +16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NOTE: The doc offers mul_generator as GENERATOR_EXTENDED * s and omits that its extended coordinates differ, which rkyv archives of stealth addresses and keys store.

Context: StealthAddress holds R as a JubJubExtended, and note_pk as a NotePublicKey that wraps one. rkyv archives their extended coordinates as they are. PublicKey holds A and B the same way, and ViewKey holds B. PublicKey::gen_stealth_address keeps GENERATOR_EXTENDED * for this reason, but only the message of 23f08e2 and the pull request description say so. No test in core pins the archived coordinates. A later change that moves gen_stealth_address to mul_generator for speed passes every test in core. By the message of 23f08e2, it then changes the genesis state that Rusk builds from seeded notes.

Fix: Add one sentence to the mul_generator doc. It states that the result has other extended coordinates than GENERATOR_EXTENDED * s, so callers use it only to compare points. A point that an rkyv archive stores keeps the double-and-add. A known-answer test of the archived bytes of a gen_stealth_address output enforces the same rule, at the cost of a fixture.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in #327: the mul_generator doc now states that its extended coordinates differ from GENERATOR_EXTENDED * s, so it is for comparing points only, and a point an rkyv archive stores keeps the GENERATOR_EXTENDED * double-and-add. I left the known-answer fixture on the archived bytes as an optional follow-up — glad to add it if you want the extra guard.

///
/// With `s` written in 64 signed digits as `s = Σ d_i · 16^i`, and with
/// `i = 8j + m`, `s · G = Σ_m 16^m · Σ_j d_(8j+m) · 2^(32j) · G`. The table
/// holds `1` to `8` times each `2^(32j) · G`, so Horner's rule over `m` takes
/// 64 additions of table entries and 28 doublings, where the double-and-add of
/// `GENERATOR_EXTENDED * s` takes 252 of each.
///
/// The operations do not depend on `s`: the digits are computed without
/// branching, every entry of a row is read and the one needed is kept by a
/// constant-time selection, and the point formulas are complete.
pub(crate) fn mul_generator(s: &JubJubScalar) -> JubJubExtended {
let digits = digits(s);

let mut acc = JubJubExtended::identity();
for m in (0..8).rev() {
for (j, row) in TABLE.iter().enumerate() {
acc += select(row, digits[8 * j + m]);
}
if m > 0 {
acc = acc.double().double().double().double();
}
}
acc
}

/// The signed digits `d_i` of `s = Σ d_i · 16^i`, each in `[-8, 8)` but the
/// last, in `[0, 1]` as `s < 2^252`.
fn digits(s: &JubJubScalar) -> [i8; 64] {
let mut digits = [0i8; 64];
for (i, byte) in s.to_bytes().into_iter().enumerate() {
digits[2 * i] = (byte & 0xf) as i8;
digits[2 * i + 1] = (byte >> 4) as i8;
}
// Move each digit from `[0, 16]` to `[-8, 8)`, carrying into the next.
for i in 0..63 {
let carry = (digits[i] + 8) >> 4;
digits[i] -= carry << 4;
digits[i + 1] += carry;
}
digits
}

/// Returns `d · B` for `d` in `[-8, 8]`, from the row `[1 · B, …, 8 · B]` of
/// the table, reading every entry of the row.
fn select(row: &[JubJubAffine; 8], d: i8) -> JubJubAffine {
// All ones for a negative `d`, zero otherwise.
let sign = d >> 7;
let abs = ((d ^ sign) - sign) as u8;

let mut point = JubJubAffine::identity();
for (k, entry) in (1..=8).zip(row) {
point.conditional_assign(entry, abs.ct_eq(&k));
}
JubJubAffine::conditional_select(
&point,
&-point,
Choice::from((sign & 1) as u8),
)
}

#[cfg(test)]
mod tests {
use dusk_jubjub::GENERATOR_EXTENDED;
use ff::Field;
use rand::SeedableRng;
use rand::rngs::StdRng;

use super::*;

#[test]
fn table_holds_the_multiples_of_the_generator() {
let mut base = GENERATOR_EXTENDED;
for (j, row) in TABLE.iter().enumerate() {
let mut multiple = base;
for (k, entry) in row.iter().enumerate() {
let expected = JubJubAffine::from(multiple);
assert_eq!(*entry, expected, "TABLE[{j}][{k}]");
multiple += base;
}
for _ in 0..32 {
base = base.double();
}
}
}

#[test]
fn mul_generator_equals_the_double_and_add() {
let nibbles = |n: u64| JubJubScalar::from_raw([n, n, n, n >> 4]);
let edge = [
JubJubScalar::zero(),
JubJubScalar::one(),
// -1, the order minus 1, the largest scalar
-JubJubScalar::one(),
-JubJubScalar::from(2u64),
JubJubScalar::from(8u64),
-JubJubScalar::from(8u64),
JubJubScalar::from(9u64),
JubJubScalar::from(u64::MAX),
// every digit below the top one 8, each carrying into the next
nibbles(0x8888_8888_8888_8888),
// every digit below the top one 7, none carrying
nibbles(0x7777_7777_7777_7777),
];

let mut rng = StdRng::seed_from_u64(0x9e7);
let random = (0..1000).map(|_| JubJubScalar::random(&mut rng));

for s in edge.into_iter().chain(random) {
assert_eq!(mul_generator(&s), GENERATOR_EXTENDED * s, "{s:?}");
}
}
}
Loading