Skip to content

Commit 63ffbf2

Browse files
authored
Merge pull request #1064 from ProvableHQ/rr-bulk-record-decryption
Add bulk record ownership and decryption methods
2 parents 6ce0477 + d40db32 commit 63ffbf2

File tree

7 files changed

+191
-13
lines changed

7 files changed

+191
-13
lines changed

docs/api_reference/sdk-src_wasm.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2926,6 +2926,18 @@ __*return*__ | [Group](sdk-src_wasm.md) | *The record nonce.*
29262926
29272927
---
29282928
2929+
### `clone() ► RecordCiphertext`
2930+
2931+
![modifier: public](images/badges/modifier-public.svg)
2932+
2933+
Clone the RecordCiphertext WASM object.
2934+
2935+
Parameters | Type | Description
2936+
--- | --- | ---
2937+
__*return*__ | [RecordCiphertext](sdk-src_wasm.md) | *A clone of the RecordCiphertext WASM object.*
2938+
2939+
---
2940+
29292941
# Class `RecordPlaintext`
29302942
29312943
Plaintext representation of an Aleo record
@@ -3160,6 +3172,18 @@ __*return*__ | [Group](sdk-src_wasm.md) | *record view key*
31603172
31613173
---
31623174
3175+
### `clone() ► RecordPlaintext`
3176+
3177+
![modifier: public](images/badges/modifier-public.svg)
3178+
3179+
Clone the RecordPlaintext WASM object.
3180+
3181+
Parameters | Type | Description
3182+
--- | --- | ---
3183+
__*return*__ | [RecordPlaintext](sdk-src_wasm.md) | *A clone of the RecordPlaintext WASM object.*
3184+
3185+
---
3186+
31633187
# Class `Scalar`
31643188
31653189
Scalar field element.

sdk/tests/data/records.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Ciphertext records
2+
const RECORD_CIPHERTEXT_STRING = "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q";
3+
const RECORD_CIPHERTEXT_STRING_COPY = "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q";
4+
const RECORD_CIPHERTEXT_STRING_NOT_OWNED = "RECORD1QVQSQ5H8YT5682E73ZT7PYNJGPL29MWTSETRVS9VHCKFHJRNX9RX94CFQYXX66TRWFHKXUN9V35HGUERQQPQZQZ6KMY7S5HPKKF02L6R46QM8RQCW9X0K4RQ6GT234AMJ2UG3LMTQT5NY4UG8SXJY3U8D05K4Q3E9F54VX67ZMD3G6JYQQ7KXRWS0R0SWM6P833";
5+
const RECORD_CIPHERTEXT_STRING_NOT_OWNED2 = "RECORD1QVQSP37HJE4CEU8EFZE8XMAHE5TDTXCZ0K534WQPKVN6C9R629X3C4Q8QYRXZMT0W4H8GGCQQGQSPVUJYCN0K7HYFHENXA40HXTFSX68092WMVJ4E3XSEXR2DY0FMCCXT0DS42W5MAASZFJV930QVQRKATQJ900AKU4K777UMH2K54ZHLUGQC2AFJD";
6+
7+
// Plaintext record
8+
const RECORD_PLAINTEXT_STRING = `{
9+
owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,
10+
microcredits: 1500000000000000u64.private,
11+
_nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public
12+
}`;
13+
14+
// View key and record view key
15+
const VIEW_KEY_STRING = "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX";
16+
const RECORD_VIEW_KEY_STRING = "4445718830394614891114647247073357094867447866913203502139893824059966201724field";
17+
18+
export {
19+
RECORD_CIPHERTEXT_STRING,
20+
RECORD_CIPHERTEXT_STRING_COPY,
21+
RECORD_CIPHERTEXT_STRING_NOT_OWNED,
22+
RECORD_CIPHERTEXT_STRING_NOT_OWNED2,
23+
RECORD_PLAINTEXT_STRING,
24+
RECORD_VIEW_KEY_STRING,
25+
VIEW_KEY_STRING,
26+
};

sdk/tests/wasm.test.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import {
1111
recordPlaintextString,
1212
beaconPrivateKeyString
1313
} from "./data/account-data.js";
14+
import {
15+
RECORD_CIPHERTEXT_STRING,
16+
RECORD_CIPHERTEXT_STRING_COPY,
17+
RECORD_CIPHERTEXT_STRING_NOT_OWNED,
18+
RECORD_CIPHERTEXT_STRING_NOT_OWNED2,
19+
RECORD_PLAINTEXT_STRING,
20+
RECORD_VIEW_KEY_STRING,
21+
VIEW_KEY_STRING,
22+
} from "./data/records.js";
1423

1524

1625
describe('WASM Objects', () => {
@@ -440,20 +449,16 @@ describe('WASM Objects', () => {
440449
}
441450
});
442451

443-
444452
describe('EncryptionToolkit', () => {
445-
const recordCiphertextString = "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q";
446-
const recordCiphertext = RecordCiphertext.fromString(recordCiphertextString);
447-
const recordPlaintextString = `{
448-
owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,
449-
microcredits: 1500000000000000u64.private,
450-
_nonce: 3077450429259593211617823051143573281856129402760267155982965992208217472983group.public
451-
}`;
452-
const recordPlaintext = RecordPlaintext.fromString(recordPlaintextString);
453-
const viewKeyString = "AViewKey1ccEt8A2Ryva5rxnKcAbn7wgTaTsb79tzkKHFpeKsm9NX";
454-
const viewKey = ViewKey.from_string(viewKeyString);
455-
const recordViewKeyString = "4445718830394614891114647247073357094867447866913203502139893824059966201724field";
456-
const recordViewKey = Field.fromString(recordViewKeyString);
453+
const recordCiphertext = RecordCiphertext.fromString(RECORD_CIPHERTEXT_STRING);
454+
const recordCiphertextNotOwned = RecordCiphertext.fromString(RECORD_CIPHERTEXT_STRING_NOT_OWNED);
455+
const recordCiphertextNotOwned2 = RecordCiphertext.fromString(RECORD_CIPHERTEXT_STRING_NOT_OWNED2);
456+
const recordCiphertextArray = [recordCiphertext, recordCiphertextNotOwned, recordCiphertextNotOwned2];
457+
const recordCiphertextArrayCopy = recordCiphertextArray.map(record => record.clone());
458+
const recordPlaintext = RecordPlaintext.fromString(RECORD_PLAINTEXT_STRING);
459+
const recordPlaintextCopy = recordPlaintext.clone();
460+
const viewKey = ViewKey.from_string(VIEW_KEY_STRING);
461+
const recordViewKey = Field.fromString(RECORD_VIEW_KEY_STRING);
457462

458463
it('can generate a record view key from a view key and a record ciphertext', () => {
459464
const generatedRecordViewKey = EncryptionToolkit.generateRecordViewKey(viewKey, recordCiphertext);
@@ -469,5 +474,15 @@ owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,
469474
const invalidRecordViewKey = Field.fromString("4445718830394614891114647247073357114867447866913203502139893824059966201724field");
470475
expect(() => EncryptionToolkit.decryptRecordWithRVk(invalidRecordViewKey, recordCiphertext)).to.throw();
471476
});
477+
it('can check if a record ciphertext from an array of record ciphertexts is owned by a view key', () => {
478+
const ownedRecords = EncryptionToolkit.checkOwnedRecords(viewKey, recordCiphertextArray);
479+
// Ensure the record ciphertext is owned by the view key
480+
expect(ownedRecords[0].toString()).equal(RECORD_CIPHERTEXT_STRING_COPY.toString());
481+
});
482+
it('can decrypt a record ciphertext from an array of record ciphertexts', () => {
483+
const decryptedRecords = EncryptionToolkit.decryptOwnedRecords(viewKey, recordCiphertextArrayCopy);
484+
// Ensure the decrypted record is the same as the plaintext
485+
expect(decryptedRecords[0].toString()).equal(recordPlaintextCopy.toString());
486+
});
472487
});
473488
});

wasm/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "@provablehq/wasm",
33
"version": "0.9.4",
44
"type": "module",
5+
56
"description": "SnarkVM WASM binaries with javascript bindings",
67
"collaborators": [
78
"The Provable Team"

wasm/src/record/record_ciphertext.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,14 @@ impl RecordCiphertext {
156156
pub fn nonce(&self) -> Group {
157157
Group::from(self.0.nonce())
158158
}
159+
160+
/// Clone the RecordCiphertext WASM object.
161+
///
162+
/// @returns {RecordCiphertext} A clone of the RecordCiphertext WASM object.
163+
#[allow(clippy::should_implement_trait)]
164+
pub fn clone(&self) -> RecordCiphertext {
165+
RecordCiphertext(self.0.clone())
166+
}
159167
}
160168

161169
impl Deref for RecordCiphertext {
@@ -235,6 +243,13 @@ mod tests {
235243
assert!(RecordCiphertext::from_string(invalid_bech32).is_err());
236244
}
237245

246+
#[wasm_bindgen_test]
247+
fn test_clone() {
248+
let record = RecordCiphertext::from_string(OWNER_CIPHERTEXT).unwrap();
249+
let cloned_record = record.clone();
250+
assert_eq!(record.to_string(), cloned_record.to_string());
251+
}
252+
238253
#[wasm_bindgen_test]
239254
fn test_decrypt_and_tag_computation() {
240255
let record = RecordCiphertext::from_string(OWNER_CIPHERTEXT).unwrap();

wasm/src/record/record_plaintext.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,14 @@ impl RecordPlaintext {
265265
pub fn record_view_key(&self, view_key: &ViewKey) -> Field {
266266
Group::from_string(&self.nonce()).unwrap().scalar_multiply(&view_key.to_scalar()).to_x_coordinate()
267267
}
268+
269+
/// Clone the RecordPlaintext WASM object.
270+
///
271+
/// @returns {RecordPlaintext} A clone of the RecordPlaintext WASM object.
272+
#[allow(clippy::should_implement_trait)]
273+
pub fn clone(&self) -> RecordPlaintext {
274+
RecordPlaintext(self.0.clone())
275+
}
268276
}
269277

270278
impl Deref for RecordPlaintext {
@@ -341,6 +349,13 @@ mod tests {
341349
assert_eq!(record.to_string(), CREDITS_RECORD);
342350
}
343351

352+
#[wasm_bindgen_test]
353+
fn test_clone() {
354+
let record = RecordPlaintext::from_string(CREDITS_RECORD).unwrap();
355+
let cloned_record = record.clone();
356+
assert_eq!(record.to_string(), cloned_record.to_string());
357+
}
358+
344359
#[wasm_bindgen_test]
345360
fn test_get_record_member() {
346361
// Get the record members.

wasm/src/utilities/encrypt.rs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use crate::{
3131
U8Native,
3232
},
3333
};
34+
use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
3435
use snarkvm_console::prelude::{FromFields, Itertools, Network, Visibility};
3536

3637
use indexmap::IndexMap;
@@ -210,6 +211,48 @@ impl EncryptionToolkit {
210211
pub fn decrypt_transition_with_vk(transition: &Transition, transition_vk: &Field) -> Result<Transition, String> {
211212
transition.decrypt_transition(transition_vk)
212213
}
214+
215+
/// Decrypts a set of record ciphertexts in parallel and stores successful decryptions.
216+
///
217+
/// @param {ViewKey} view_key The view key of the owner of the records.
218+
/// @param {Vec<RecordCiphertext>} records The record ciphertexts to decrypt.
219+
///
220+
/// @returns {vec<RecordPlaintext>} The decrypted record plaintexts.
221+
#[wasm_bindgen(js_name = "decryptOwnedRecords")]
222+
pub fn decrypt_owned_records(
223+
view_key: &ViewKey,
224+
records: Vec<RecordCiphertext>,
225+
) -> Result<Vec<RecordPlaintext>, String> {
226+
// Use Rayon to parallelize the decryption process and store successful decryptions.
227+
let decrypted_records: Vec<RecordPlaintext> = records
228+
.par_iter()
229+
.filter_map(|record| {
230+
let record_vk = Self::generate_record_view_key(view_key, record).ok()?;
231+
let decrypted_record = Self::decrypt_record_symmetric_unchecked(&record_vk, record).ok()?;
232+
Some(decrypted_record)
233+
})
234+
.collect();
235+
236+
Ok(decrypted_records)
237+
}
238+
239+
/// Checks if a record ciphertext is owned by the given view key.
240+
///
241+
/// @param {ViewKey} view_key View key of the owner of the records.
242+
/// @param {Vec<RecordCiphertext>} records The record ciphertexts for which to check ownership.
243+
///
244+
/// @returns {Vec<RecordCiphertext>} The record ciphertexts that are owned by the view key.
245+
#[wasm_bindgen(js_name = "checkOwnedRecords")]
246+
pub fn check_owned_records(
247+
view_key: &ViewKey,
248+
records: Vec<RecordCiphertext>,
249+
) -> Result<Vec<RecordCiphertext>, String> {
250+
// Use Rayon to parallelize the ownership check.
251+
let owned_records: Vec<RecordCiphertext> =
252+
records.into_par_iter().filter(|record| record.is_owner(view_key)).collect();
253+
254+
Ok(owned_records)
255+
}
213256
}
214257

215258
#[cfg(test)]
@@ -221,6 +264,8 @@ mod tests {
221264

222265
const NON_OWNER_VIEW_KEY: &str = "AViewKey1e2WyreaH5H4RBcioLL2GnxvHk5Ud46EtwycnhTdXLmXp";
223266
const OWNER_CIPHERTEXT: &str = "record1qyqsqpe2szk2wwwq56akkwx586hkndl3r8vzdwve32lm7elvphh37rsyqyxx66trwfhkxun9v35hguerqqpqzqrtjzeu6vah9x2me2exkgege824sd8x2379scspmrmtvczs0d93qttl7y92ga0k0rsexu409hu3vlehe3yxjhmey3frh2z5pxm5cmxsv4un97q";
267+
const NON_OWNED_CIPHERTEXT_1: &str = "RECORD1QVQSQ5H8YT5682E73ZT7PYNJGPL29MWTSETRVS9VHCKFHJRNX9RX94CFQYXX66TRWFHKXUN9V35HGUERQQPQZQZ6KMY7S5HPKKF02L6R46QM8RQCW9X0K4RQ6GT234AMJ2UG3LMTQT5NY4UG8SXJY3U8D05K4Q3E9F54VX67ZMD3G6JYQQ7KXRWS0R0SWM6P833";
268+
const NON_OWNED_CIPHERTEXT_2: &str = "RECORD1QVQSP37HJE4CEU8EFZE8XMAHE5TDTXCZ0K534WQPKVN6C9R629X3C4Q8QYRXZMT0W4H8GGCQQGQSPVUJYCN0K7HYFHENXA40HXTFSX68092WMVJ4E3XSEXR2DY0FMCCXT0DS42W5MAASZFJV930QVQRKATQJ900AKU4K777UMH2K54ZHLUGQC2AFJD";
224269
const OWNER_PLAINTEXT: &str = r"{
225270
owner: aleo1j7qxyunfldj2lp8hsvy7mw5k8zaqgjfyr72x2gh3x4ewgae8v5gscf5jh3.private,
226271
microcredits: 1500000000000000u64.private,
@@ -284,4 +329,41 @@ mod tests {
284329
let result = EncryptionToolkit::decrypt_record_symmetric_unchecked(&record_vk, &owner_ciphertext);
285330
assert!(result.is_err(), "Decryption should fail with a non-owner's view key");
286331
}
332+
333+
#[wasm_bindgen_test]
334+
fn test_bulk_record_decryption() {
335+
let owned_ciphertext = RecordCiphertext::from_str(OWNER_CIPHERTEXT).unwrap();
336+
//Need to add these two non-owned ciphertexts for testing
337+
let nonowned_ciphertext_1 = RecordCiphertext::from_str(NON_OWNED_CIPHERTEXT_1).unwrap();
338+
let nonowned_ciphertext_2 = RecordCiphertext::from_str(NON_OWNED_CIPHERTEXT_2).unwrap();
339+
340+
let records: Vec<RecordCiphertext> = vec![owned_ciphertext, nonowned_ciphertext_1, nonowned_ciphertext_2];
341+
let owner_view_key = ViewKey::from_str(OWNER_VIEW_KEY).unwrap();
342+
343+
// Decrypt the owned records
344+
let decrypted_records = EncryptionToolkit::decrypt_owned_records(&owner_view_key, records).unwrap();
345+
// Verify that only the owned record was decrypted
346+
assert_eq!(decrypted_records.len(), 1, "Only one record should be decrypted");
347+
assert_eq!(
348+
decrypted_records[0].to_string(),
349+
OWNER_PLAINTEXT,
350+
"Decrypted record should match the owner's plaintext"
351+
);
352+
}
353+
354+
#[wasm_bindgen_test]
355+
fn test_check_owned_records() {
356+
let owned_ciphertext = RecordCiphertext::from_str(OWNER_CIPHERTEXT).unwrap();
357+
// Need to add these two non-owned ciphertexts for testing
358+
let nonowned_ciphertext_1 = RecordCiphertext::from_str(NON_OWNED_CIPHERTEXT_1).unwrap();
359+
let nonowned_ciphertext_2 = RecordCiphertext::from_str(NON_OWNED_CIPHERTEXT_2).unwrap();
360+
let records: Vec<RecordCiphertext> = vec![owned_ciphertext, nonowned_ciphertext_1, nonowned_ciphertext_2];
361+
let owner_view_key = ViewKey::from_str(OWNER_VIEW_KEY).unwrap();
362+
363+
// Check owned records
364+
let owned_records = EncryptionToolkit::check_owned_records(&owner_view_key, records).unwrap();
365+
// Verify that only the owned record is returned
366+
assert_eq!(owned_records.len(), 1, "Only one owned record should be returned");
367+
assert_eq!(owned_records[0].to_string(), OWNER_CIPHERTEXT, "Owned record should match the owner's ciphertext");
368+
}
287369
}

0 commit comments

Comments
 (0)