Skip to content

Commit

Permalink
Use an optimized method for note decryption when multiple accounts ar…
Browse files Browse the repository at this point in the history
…e present

When multiple accounts are present, instead of decrypting each note for
each account, one by one, use an optimized method that can save some
computation cycles.
  • Loading branch information
andiflabs committed Jul 9, 2024
1 parent fbe79db commit 9301143
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ homepage = "https://ironfish.network/"
repository = "https://github.com/iron-fish/ironfish"

[profile.release]
debug = true
debug = true
2 changes: 1 addition & 1 deletion ironfish-rust-nodejs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ ironfish = { path = "../ironfish-rust" }
ironfish-frost = { git = "https://github.com/iron-fish/ironfish-frost.git", branch = "main" }
napi = { version = "2.13.2", features = ["napi6"] }
napi-derive = "2.13.0"
jubjub = { git = "https://github.com/iron-fish/jubjub.git", branch = "blstrs" }
jubjub = { git = "https://github.com/iron-fish/jubjub.git", branch = "blstrs", features = ["multiply-many"] }
rand = "0.8.5"
num_cpus = "1.16.0"
signal-hook = { version = "0.3.17", optional = true, default-features = false, features = ["iterator"] }
Expand Down
1 change: 1 addition & 0 deletions ironfish-rust-nodejs/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export class NoteEncrypted {
static combineHash(depth: number, jsLeft: Buffer, jsRight: Buffer): Buffer
/** Returns undefined if the note was unable to be decrypted with the given key. */
decryptNoteForOwner(incomingHexKey: string): Buffer | null
decryptNoteForOwners(incomingHexKeys: Array<string>): Array<Buffer | undefined | null>
/** Returns undefined if the note was unable to be decrypted with the given key. */
decryptNoteForSpender(outgoingHexKey: string): Buffer | null
}
Expand Down
3 changes: 3 additions & 0 deletions ironfish-rust-nodejs/src/stats.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ fn print_stats(colors: bool) {
{highlight}Elliptic Curve Point Multiplication Stats:\n\
• affine muls: {affine_muls}\n\
• extended muls: {extended_muls}\n\
• extended vector muls: {extended_mul_many_calls} calls / {extended_mul_many_operands} points\n\
Note Encryption Stats:\n\
• total: {note_construct}\n\
Note Decryption Stats:\n\
Expand All @@ -39,6 +40,8 @@ fn print_stats(colors: bool) {
reset = if colors { "\x1b[0m" } else { "" },
affine_muls = ecpm_stats.affine_muls,
extended_muls = ecpm_stats.extended_muls,
extended_mul_many_calls = ecpm_stats.extended_mul_many_calls,
extended_mul_many_operands = ecpm_stats.extended_mul_many_operands,
note_construct = note_stats.construct,
note_dec_for_owner = note_stats.decrypt_note_for_owner.total,
note_dec_for_owner_ok = note_stats.decrypt_note_for_owner.successful,
Expand Down
73 changes: 49 additions & 24 deletions ironfish-rust-nodejs/src/structs/note_encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use crate::to_napi_err;
use ironfish::merkle_note::NOTE_ENCRYPTION_KEY_SIZE;
use ironfish::note::ENCRYPTED_NOTE_SIZE;
use ironfish::note::PLAINTEXT_NOTE_SIZE;
use ironfish::serializing::aead::MAC_SIZE;
use ironfish::IncomingViewKey;
use ironfish::MerkleNote;
use ironfish::MerkleNoteHash;
use ironfish::Note;
use ironfish::OutgoingViewKey;
use napi::bindgen_prelude::*;
use napi::JsBuffer;
use napi_derive::napi;

use ironfish::merkle_note::NOTE_ENCRYPTION_KEY_SIZE;
use ironfish::note::ENCRYPTED_NOTE_SIZE;
use ironfish::serializing::aead::MAC_SIZE;
use ironfish::MerkleNote;

use crate::to_napi_err;

#[napi]
pub const NOTE_ENCRYPTION_KEY_LENGTH: u32 = NOTE_ENCRYPTION_KEY_SIZE as u32;

Expand All @@ -29,6 +29,33 @@ pub const ENCRYPTED_NOTE_PLAINTEXT_LENGTH: u32 = ENCRYPTED_NOTE_SIZE as u32 + MA
pub const ENCRYPTED_NOTE_LENGTH: u32 =
NOTE_ENCRYPTION_KEY_LENGTH + ENCRYPTED_NOTE_PLAINTEXT_LENGTH + 96;

#[inline]
fn try_map<T, I, F, R, E>(items: I, f: F) -> std::result::Result<Vec<R>, E>
where
I: IntoIterator<Item = T>,
I::IntoIter: ExactSizeIterator,
F: Fn(T) -> std::result::Result<R, E>,
{
let items = items.into_iter();
let mut result = Vec::with_capacity(items.len());
for item in items {
result.push(f(item)?);
}
Ok(result)
}

#[inline]
fn decrypted_note_to_buffer<E>(note: std::result::Result<Note, E>) -> Result<Option<Buffer>> {
match note {
Ok(note) => {
let mut buf = [0u8; PLAINTEXT_NOTE_SIZE];
note.write(&mut buf[..]).map_err(to_napi_err)?;
Ok(Some(Buffer::from(&buf[..])))
}
Err(_) => Ok(None),
}
}

#[napi(js_name = "NoteEncrypted")]
pub struct NativeNoteEncrypted {
pub(crate) note: MerkleNote,
Expand Down Expand Up @@ -110,31 +137,29 @@ impl NativeNoteEncrypted {
pub fn decrypt_note_for_owner(&self, incoming_hex_key: String) -> Result<Option<Buffer>> {
let incoming_view_key =
IncomingViewKey::from_hex(&incoming_hex_key).map_err(to_napi_err)?;
let decrypted_note = self.note.decrypt_note_for_owner(&incoming_view_key);
decrypted_note_to_buffer(decrypted_note).map_err(to_napi_err)
}

Ok(match self.note.decrypt_note_for_owner(&incoming_view_key) {
Ok(note) => {
let mut vec = vec![];
note.write(&mut vec).map_err(to_napi_err)?;
Some(Buffer::from(vec))
}
Err(_) => None,
#[napi]
pub fn decrypt_note_for_owners(
&self,
incoming_hex_keys: Vec<String>,
) -> Result<Vec<Option<Buffer>>> {
let incoming_view_keys = try_map(&incoming_hex_keys[..], |hex_key| {
IncomingViewKey::from_hex(hex_key)
})
.map_err(to_napi_err)?;
let decrypted_notes = self.note.decrypt_note_for_owners(&incoming_view_keys);
try_map(decrypted_notes, decrypted_note_to_buffer).map_err(to_napi_err)
}

/// Returns undefined if the note was unable to be decrypted with the given key.
#[napi]
pub fn decrypt_note_for_spender(&self, outgoing_hex_key: String) -> Result<Option<Buffer>> {
let outgoing_view_key =
OutgoingViewKey::from_hex(&outgoing_hex_key).map_err(to_napi_err)?;
Ok(
match self.note.decrypt_note_for_spender(&outgoing_view_key) {
Ok(note) => {
let mut vec = vec![];
note.write(&mut vec).map_err(to_napi_err)?;
Some(Buffer::from(vec))
}
Err(_) => None,
},
)
let decrypted_note = self.note.decrypt_note_for_spender(&outgoing_view_key);
decrypted_note_to_buffer(decrypted_note).map_err(to_napi_err)
}
}
2 changes: 1 addition & 1 deletion ironfish-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ group = "0.12.0"
ironfish-frost = { git = "https://github.com/iron-fish/ironfish-frost.git", branch = "main" }
fish_hash = "0.3.0"
ironfish_zkp = { version = "0.2.0", path = "../ironfish-zkp" }
jubjub = { git = "https://github.com/iron-fish/jubjub.git", branch = "blstrs" }
jubjub = { git = "https://github.com/iron-fish/jubjub.git", branch = "blstrs", features = ["multiply-many"] }
lazy_static = "1.4.0"
libc = "0.2.126" # sub-dependency that needs a pinned version until a new release of cpufeatures: https://github.com/RustCrypto/utils/pull/789
rand = "0.8.5"
Expand Down
46 changes: 44 additions & 2 deletions ironfish-rust/src/keys/view_keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ impl IncomingViewKey {
pub(crate) fn shared_secret(&self, ephemeral_public_key: &SubgroupPoint) -> [u8; 32] {
shared_secret(&self.view_key, ephemeral_public_key, ephemeral_public_key)
}

pub(crate) fn shared_secrets(
slice: &[Self],
ephemeral_public_key: &SubgroupPoint,
) -> Vec<[u8; 32]> {
let raw_view_keys = slice
.into_iter()
.map(move |ivk| ivk.view_key.to_bytes())
.collect::<Vec<[u8; 32]>>();
shared_secrets(
&raw_view_keys[..],
ephemeral_public_key,
ephemeral_public_key,
)
}
}

/// Contains two keys that are required (along with outgoing view key)
Expand Down Expand Up @@ -217,23 +232,50 @@ impl OutgoingViewKey {
/// key (Bob's public key) to get the final shared secret
///
/// The resulting key can be used in any symmetric cipher
#[must_use]
pub(crate) fn shared_secret(
secret_key: &jubjub::Fr,
other_public_key: &SubgroupPoint,
reference_public_key: &SubgroupPoint,
) -> [u8; 32] {
let shared_secret = (other_public_key * secret_key).to_bytes();
hash_shared_secret(&shared_secret, reference_public_key)
}

/// Equivalent to calling `shared_secret()` multiple times on the same
/// `other_public_key`/`reference_public_key`, but more efficient.
#[must_use]
pub(crate) fn shared_secrets(
secret_keys: &[[u8; 32]],
other_public_key: &SubgroupPoint,
reference_public_key: &SubgroupPoint,
) -> Vec<[u8; 32]> {
let shared_secrets = other_public_key
.as_extended()
.multiply_many(&secret_keys[..]);
shared_secrets
.into_iter()
.map(move |shared_secret| {
hash_shared_secret(&shared_secret.to_bytes(), reference_public_key)
})
.collect()
}

#[inline]
#[must_use]
fn hash_shared_secret(shared_secret: &[u8; 32], reference_public_key: &SubgroupPoint) -> [u8; 32] {
let reference_bytes = reference_public_key.to_bytes();

let mut hasher = Blake2b::new()
.hash_length(32)
.personal(DIFFIE_HELLMAN_PERSONALIZATION)
.to_state();

hasher.update(&shared_secret);
hasher.update(&shared_secret[..]);
hasher.update(&reference_bytes);

let mut hash_result = [0; 32];
hash_result[..].clone_from_slice(hasher.finalize().as_ref());
hash_result[..].copy_from_slice(hasher.finalize().as_ref());
hash_result
}

Expand Down
42 changes: 36 additions & 6 deletions ironfish-rust/src/merkle_note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,18 +212,48 @@ impl MerkleNote {
owner_view_key: &IncomingViewKey,
) -> Result<Note, IronfishError> {
#[cfg(feature = "note-encryption-stats")]
stats::inc_decrypt_note_for_owner();
stats::inc_decrypt_note_for_owner(1);

let shared_secret = owner_view_key.shared_secret(&self.ephemeral_public_key);
let note =
Note::from_owner_encrypted(owner_view_key, &shared_secret, &self.encrypted_note)?;
note.verify_commitment(self.note_commitment)?;

#[cfg(feature = "note-encryption-stats")]
stats::inc_decrypt_note_for_owner_ok();
stats::inc_decrypt_note_for_owner_ok(1);
Ok(note)
}

pub fn decrypt_note_for_owners(
&self,
owner_view_keys: &[IncomingViewKey],
) -> Vec<Result<Note, IronfishError>> {
#[cfg(feature = "note-encryption-stats")]
stats::inc_decrypt_note_for_owner(owner_view_keys.len());

let shared_secrets =
IncomingViewKey::shared_secrets(owner_view_keys, &self.ephemeral_public_key);

let result = owner_view_keys
.iter()
.zip(shared_secrets.iter())
.map(move |(owner_view_key, shared_secret)| {
let note = Note::from_owner_encrypted(
owner_view_key,
shared_secret,
&self.encrypted_note,
)?;
note.verify_commitment(self.note_commitment)?;
Ok(note)
})
.collect::<Vec<Result<Note, IronfishError>>>();

#[cfg(feature = "note-encryption-stats")]
stats::inc_decrypt_note_for_owner_ok(result.iter().filter(move |res| res.is_ok()).count());

result
}

pub fn decrypt_note_for_spender(
&self,
spender_key: &OutgoingViewKey,
Expand Down Expand Up @@ -353,13 +383,13 @@ pub mod stats {
}

#[inline(always)]
pub(super) fn inc_decrypt_note_for_owner() {
DECRYPT_FOR_OWNER_CALLS.fetch_add(1, Relaxed);
pub(super) fn inc_decrypt_note_for_owner(count: usize) {
DECRYPT_FOR_OWNER_CALLS.fetch_add(count, Relaxed);
}

#[inline(always)]
pub(super) fn inc_decrypt_note_for_owner_ok() {
DECRYPT_FOR_OWNER_OK_CALLS.fetch_add(1, Relaxed);
pub(super) fn inc_decrypt_note_for_owner_ok(count: usize) {
DECRYPT_FOR_OWNER_OK_CALLS.fetch_add(count, Relaxed);
}

#[inline(always)]
Expand Down
17 changes: 11 additions & 6 deletions ironfish-rust/src/note.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ pub const ENCRYPTED_NOTE_SIZE: usize =
// + 32 memo
// + 32 sender address
// = 136
pub const PLAINTEXT_NOTE_SIZE: usize = PUBLIC_ADDRESS_SIZE
+ ASSET_ID_LENGTH
+ AMOUNT_VALUE_SIZE
+ SCALAR_SIZE
+ MEMO_SIZE
+ PUBLIC_ADDRESS_SIZE;
pub const SCALAR_SIZE: usize = 32;
pub const MEMO_SIZE: usize = 32;
pub const AMOUNT_VALUE_SIZE: usize = 8;
Expand Down Expand Up @@ -105,7 +111,7 @@ pub struct Note {
pub(crate) sender: PublicAddress,
}

impl<'a> Note {
impl Note {
/// Construct a new Note.
pub fn new(
owner: PublicAddress,
Expand Down Expand Up @@ -158,7 +164,7 @@ impl<'a> Note {
/// This should generally never be used to serialize to disk or the network.
/// It is primarily added as a device for transmitting the note across
/// thread boundaries.
pub fn write<W: io::Write>(&self, mut writer: &mut W) -> Result<(), IronfishError> {
pub fn write<W: io::Write>(&self, mut writer: W) -> Result<(), IronfishError> {
self.owner.write(&mut writer)?;
self.asset_id.write(&mut writer)?;
writer.write_u64::<LittleEndian>(self.value)?;
Expand All @@ -179,7 +185,7 @@ impl<'a> Note {
/// This function allows the owner to decrypt the note using the derived
/// shared secret and their own view key.
pub fn from_owner_encrypted(
owner_view_key: &'a IncomingViewKey,
owner_view_key: &IncomingViewKey,
shared_secret: &[u8; 32],
encrypted_bytes: &[u8; ENCRYPTED_NOTE_SIZE + aead::MAC_SIZE],
) -> Result<Self, IronfishError> {
Expand Down Expand Up @@ -349,10 +355,9 @@ impl<'a> Note {
) -> Result<(jubjub::Fr, AssetIdentifier, u64, Memo, PublicAddress), IronfishError> {
let plaintext_bytes: [u8; ENCRYPTED_NOTE_SIZE] =
aead::decrypt(shared_secret, encrypted_bytes)?;
let mut reader = &plaintext_bytes[..];

let mut reader = plaintext_bytes[..].as_ref();

let randomness: jubjub::Fr = read_scalar(&mut reader)?;
let randomness = read_scalar(&mut reader)?;
let value = reader.read_u64::<LittleEndian>()?;

let mut memo = Memo::default();
Expand Down
12 changes: 12 additions & 0 deletions ironfish/src/primitives/noteEncrypted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ export class NoteEncrypted {
}
}

decryptNoteForOwners(ownerHexKeys: Array<string>): Array<Note | undefined> {
if (ownerHexKeys.length === 0) {
return []
} else if (ownerHexKeys.length === 1) {
return [this.decryptNoteForOwner(ownerHexKeys[0])]
}

const notes = this.takeReference().decryptNoteForOwners(ownerHexKeys)
this.returnReference()
return notes.map((note) => (note ? new Note(note) : undefined))
}

decryptNoteForSpender(spenderHexKey: string): Note | undefined {
const note = this.takeReference().decryptNoteForSpender(spenderHexKey)
this.returnReference()
Expand Down
9 changes: 7 additions & 2 deletions ironfish/src/workerPool/tasks/decryptNotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,14 @@ export class DecryptNotesTask extends WorkerTask {
skipValidation: options.skipNoteValidation,
})

for (const { incomingViewKey, outgoingViewKey, viewKey } of accountKeys) {
const receivedNotes = note.decryptNoteForOwners(
accountKeys.map(({ incomingViewKey }) => incomingViewKey),
)

for (const [i, receivedNote] of receivedNotes.entries()) {
// Try decrypting the note as the owner
const receivedNote = note.decryptNoteForOwner(incomingViewKey)
if (receivedNote && receivedNote.value() !== 0n) {
const { viewKey } = accountKeys[i]
decryptedNotes.push({
index: currentNoteIndex,
forSpender: false,
Expand All @@ -304,6 +308,7 @@ export class DecryptNotesTask extends WorkerTask {

if (options.decryptForSpender) {
// Try decrypting the note as the spender
const { outgoingViewKey } = accountKeys[i]
const spentNote = note.decryptNoteForSpender(outgoingViewKey)
if (spentNote && spentNote.value() !== 0n) {
decryptedNotes.push({
Expand Down

0 comments on commit 9301143

Please sign in to comment.