extern crate alloc;
#[cfg(any(feature = "fhe-state", feature = "wasm"))]
use alloc::vec::Vec;
#[cfg(feature = "fhe-state")]
use core::cell::RefCell;
#[cfg(feature = "fhe-state")]
use serde::{Deserialize, Serialize};
use sha3::{
digest::{ExtendableOutput, Update, XofReader},
Shake256,
};
pub const ERR_OK: u32 = 0;
pub const ERR_NOT_FOUND: u32 = 1;
pub const ERR_INSUFFICIENT_BALANCE: u32 = 2;
pub const ERR_DESER: u32 = 3;
pub const ERR_INVALID_KEY: u32 = 4;
pub const ERR_UNAUTHORIZED: u32 = 5;
pub const ERR_WIRE_VERIFY_FAILED: u32 = 6;
pub fn derive_vault_id(plp_projection_bytes: &[u8]) -> [u8; 32] {
let mut hasher = Shake256::default();
hasher.update(b"AETHEL_VAULT_ID_V1");
hasher.update(plp_projection_bytes);
let mut xof = hasher.finalize_xof();
let mut vault_id = [0u8; 32];
xof.read(&mut vault_id);
vault_id
}
#[cfg(feature = "fhe-state")]
pub const VAULT_STATE_VERSION: u8 = 2;
#[derive(Serialize, Deserialize)]
#[cfg(feature = "fhe-state")]
pub struct VaultState {
pub balances: Vec<([u8; 32], Vec<u8>)>,
pub identity_projections: Vec<([u8; 32], Vec<u8>)>,
}
#[cfg(feature = "fhe-state")]
impl VaultState {
fn new() -> Self {
VaultState {
balances: Vec::new(),
identity_projections: Vec::new(),
}
}
}
#[cfg(feature = "fhe-state")]
#[derive(Default)]
struct RuntimeKeys {
server_key_bytes: Option<Vec<u8>>,
}
#[cfg(feature = "fhe-state")]
thread_local! {
static RUNTIME_KEYS: RefCell<RuntimeKeys> = RefCell::new(RuntimeKeys::default());
}
#[cfg(feature = "fhe-state")]
thread_local! {
static STATE: RefCell<Option<VaultState>> = RefCell::new(None);
}
#[cfg(feature = "fhe-state")]
fn with_state_mut<F, R>(f: F) -> R
where
F: FnOnce(&mut VaultState) -> R,
{
STATE.with(|s| {
let mut borrow = s.borrow_mut();
let state = borrow.get_or_insert_with(VaultState::new);
f(state)
})
}
#[cfg(all(target_arch = "wasm32", feature = "fhe-state"))]
mod host_fhe {
#[link(wasm_import_module = "env")]
extern "C" {
pub fn host_fhe_sub(
a_ptr: *const u8,
a_len: usize,
b_ptr: *const u8,
b_len: usize,
out_ptr: *mut u8,
out_max: usize,
) -> usize;
pub fn host_fhe_add(
a_ptr: *const u8,
a_len: usize,
b_ptr: *const u8,
b_len: usize,
out_ptr: *mut u8,
out_max: usize,
) -> usize;
pub fn host_fhe_ge(
a_ptr: *const u8,
a_len: usize,
b_ptr: *const u8,
b_len: usize,
out_ptr: *mut u8,
out_max: usize,
) -> usize;
pub fn host_fhe_select(
cond_ptr: *const u8,
cond_len: usize,
true_ptr: *const u8,
true_len: usize,
false_ptr: *const u8,
false_len: usize,
out_ptr: *mut u8,
out_max: usize,
) -> usize;
pub fn host_fhe_zero(out_ptr: *mut u8, out_max: usize) -> usize;
}
}
#[cfg(all(target_arch = "wasm32", feature = "fhe-state"))]
fn do_homomorphic_transfer(
sender_ct: &[u8],
receiver_ct: &[u8],
transfer_ct: &[u8],
) -> Option<(Vec<u8>, Vec<u8>)> {
const BUF_SIZE: usize = 65536;
unsafe {
let mut ge_buf = alloc::vec![0u8; BUF_SIZE];
let ge_len = host_fhe::host_fhe_ge(
sender_ct.as_ptr(),
sender_ct.len(),
transfer_ct.as_ptr(),
transfer_ct.len(),
ge_buf.as_mut_ptr(),
BUF_SIZE,
);
if ge_len == 0 {
return None;
}
ge_buf.truncate(ge_len);
let mut zero_buf = alloc::vec![0u8; BUF_SIZE];
let zero_len = host_fhe::host_fhe_zero(zero_buf.as_mut_ptr(), BUF_SIZE);
if zero_len == 0 {
return None;
}
zero_buf.truncate(zero_len);
let mut deduction_buf = alloc::vec![0u8; BUF_SIZE];
let deduction_len = host_fhe::host_fhe_select(
ge_buf.as_ptr(),
ge_buf.len(),
transfer_ct.as_ptr(),
transfer_ct.len(),
zero_buf.as_ptr(),
zero_buf.len(),
deduction_buf.as_mut_ptr(),
BUF_SIZE,
);
if deduction_len == 0 {
return None;
}
deduction_buf.truncate(deduction_len);
let mut new_sender_buf = alloc::vec![0u8; BUF_SIZE];
let new_sender_len = host_fhe::host_fhe_sub(
sender_ct.as_ptr(),
sender_ct.len(),
deduction_buf.as_ptr(),
deduction_buf.len(),
new_sender_buf.as_mut_ptr(),
BUF_SIZE,
);
if new_sender_len == 0 {
return None;
}
new_sender_buf.truncate(new_sender_len);
let mut new_receiver_buf = alloc::vec![0u8; BUF_SIZE];
let new_receiver_len = host_fhe::host_fhe_add(
receiver_ct.as_ptr(),
receiver_ct.len(),
deduction_buf.as_ptr(),
deduction_buf.len(),
new_receiver_buf.as_mut_ptr(),
BUF_SIZE,
);
if new_receiver_len == 0 {
return None;
}
new_receiver_buf.truncate(new_receiver_len);
Some((new_sender_buf, new_receiver_buf))
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fhe-state"))]
fn do_homomorphic_transfer(
sender_ct_bytes: &[u8],
receiver_ct_bytes: &[u8],
transfer_ct_bytes: &[u8],
server_key_bytes: &[u8],
) -> Option<(Vec<u8>, Vec<u8>)> {
use tfhe::{prelude::*, FheUint64, ServerKey};
let server_key: ServerKey = bincode::deserialize(server_key_bytes).ok()?;
tfhe::set_server_key(server_key);
let sender_balance: FheUint64 = bincode::deserialize(sender_ct_bytes).ok()?;
let receiver_balance: FheUint64 = bincode::deserialize(receiver_ct_bytes).ok()?;
let transfer_amount: FheUint64 = bincode::deserialize(transfer_ct_bytes).ok()?;
let is_sufficient = sender_balance.ge(&transfer_amount);
let zero: FheUint64 = FheUint64::encrypt_trivial(0u64);
let actual_deduction = is_sufficient.if_then_else(&transfer_amount, &zero);
let new_sender = sender_balance - &actual_deduction;
let new_receiver = receiver_balance + &actual_deduction;
let new_sender_bytes = bincode::serialize(&new_sender).ok()?;
let new_receiver_bytes = bincode::serialize(&new_receiver).ok()?;
Some((new_sender_bytes, new_receiver_bytes))
}
#[no_mangle]
#[cfg(feature = "fhe-state")]
pub extern "C" fn init_vault(server_key_bytes_ptr: *const u8, len: usize) -> u32 {
let key_bytes = unsafe { core::slice::from_raw_parts(server_key_bytes_ptr, len) };
#[cfg(not(target_arch = "wasm32"))]
{
use tfhe::ServerKey;
let _: ServerKey = match bincode::deserialize(key_bytes) {
Ok(k) => k,
Err(_) => return ERR_DESER,
};
}
RUNTIME_KEYS.with(|k| {
k.borrow_mut().server_key_bytes = Some(key_bytes.to_vec());
});
ERR_OK
}
#[no_mangle]
#[cfg(not(feature = "fhe-state"))]
pub extern "C" fn init_vault(_server_key_bytes_ptr: *const u8, _len: usize) -> u32 {
ERR_DESER
}
#[no_mangle]
#[cfg(feature = "fhe-state")]
pub extern "C" fn register_vault_ciphertext(
vault_id_ptr: *const u8,
vault_id_len: usize,
initial_ct_ptr: *const u8,
initial_ct_len: usize,
) -> u32 {
let vault_id_slice = unsafe { core::slice::from_raw_parts(vault_id_ptr, vault_id_len) };
let ct_bytes = unsafe { core::slice::from_raw_parts(initial_ct_ptr, initial_ct_len) };
if vault_id_slice.len() != 32 {
return ERR_DESER;
}
let mut vault_id = [0u8; 32];
vault_id.copy_from_slice(vault_id_slice);
with_state_mut(|state| {
state.balances.push((vault_id, ct_bytes.to_vec()));
});
ERR_OK
}
#[no_mangle]
#[cfg(not(feature = "fhe-state"))]
pub extern "C" fn register_vault_ciphertext(
_vault_id_ptr: *const u8,
_vault_id_len: usize,
_initial_ct_ptr: *const u8,
_initial_ct_len: usize,
) -> u32 {
ERR_DESER
}
#[no_mangle]
#[cfg(feature = "fhe-state")]
pub extern "C" fn homomorphic_transfer(
sender_id_ptr: *const u8,
sender_id_len: usize,
receiver_id_ptr: *const u8,
receiver_id_len: usize,
transfer_ct_ptr: *const u8,
transfer_ct_len: usize,
) -> u32 {
let sender_id_slice = unsafe { core::slice::from_raw_parts(sender_id_ptr, sender_id_len) };
let receiver_id_slice =
unsafe { core::slice::from_raw_parts(receiver_id_ptr, receiver_id_len) };
let transfer_ct = unsafe { core::slice::from_raw_parts(transfer_ct_ptr, transfer_ct_len) };
if sender_id_slice.len() != 32 || receiver_id_slice.len() != 32 {
return ERR_DESER;
}
let mut sender_id = [0u8; 32];
let mut receiver_id = [0u8; 32];
sender_id.copy_from_slice(sender_id_slice);
receiver_id.copy_from_slice(receiver_id_slice);
let indices_opt: Option<(usize, usize)> = STATE.with(|s| {
let borrow = s.borrow();
let state = borrow.as_ref()?;
let si = state.balances.iter().position(|(id, _)| *id == sender_id)?;
let ri = state
.balances
.iter()
.position(|(id, _)| *id == receiver_id)?;
Some((si, ri))
});
let (sender_idx, receiver_idx) = match indices_opt {
Some(pair) => pair,
None => return ERR_NOT_FOUND,
};
let (sender_ct_bytes, receiver_ct_bytes) = STATE.with(|s| {
let borrow = s.borrow();
let state = borrow.as_ref().unwrap();
(
state.balances[sender_idx].1.clone(),
state.balances[receiver_idx].1.clone(),
)
});
#[cfg(target_arch = "wasm32")]
let result = do_homomorphic_transfer(&sender_ct_bytes, &receiver_ct_bytes, transfer_ct);
#[cfg(not(target_arch = "wasm32"))]
let result = {
let server_key_bytes_opt: Option<Vec<u8>> =
RUNTIME_KEYS.with(|k| k.borrow().server_key_bytes.clone());
let server_key_bytes = match server_key_bytes_opt {
Some(b) => b,
None => return ERR_INVALID_KEY,
};
do_homomorphic_transfer(
&sender_ct_bytes,
&receiver_ct_bytes,
transfer_ct,
&server_key_bytes,
)
};
match result {
Some((new_sender_bytes, new_receiver_bytes)) => {
with_state_mut(|state| {
state.balances[sender_idx].1 = new_sender_bytes;
state.balances[receiver_idx].1 = new_receiver_bytes;
});
ERR_OK
}
None => ERR_DESER,
}
}
#[no_mangle]
#[cfg(not(feature = "fhe-state"))]
pub extern "C" fn homomorphic_transfer(
_sender_id_ptr: *const u8,
_sender_id_len: usize,
_receiver_id_ptr: *const u8,
_receiver_id_len: usize,
_transfer_ct_ptr: *const u8,
_transfer_ct_len: usize,
) -> u32 {
ERR_INVALID_KEY
}
#[cfg(feature = "fhe-state")]
thread_local! {
static EXPORT_BUFFER: RefCell<Vec<u8>> = RefCell::new(Vec::new());
}
#[cfg(feature = "fhe-state")]
pub fn export_vault_state_inner() -> Vec<u8> {
STATE.with(|s| {
let borrow = s.borrow();
match borrow.as_ref() {
Some(state) => {
let mut out = Vec::with_capacity(1);
out.push(VAULT_STATE_VERSION);
out.extend(bincode::serialize(state).unwrap_or_default());
out
}
None => Vec::new(),
}
})
}
#[no_mangle]
#[cfg(feature = "fhe-state")]
pub extern "C" fn export_vault_state_len() -> usize {
let bytes = export_vault_state_inner();
EXPORT_BUFFER.with(|b| {
*b.borrow_mut() = bytes;
});
EXPORT_BUFFER.with(|b| b.borrow().len())
}
#[no_mangle]
#[allow(missing_docs)]
#[cfg(feature = "fhe-state")]
pub extern "C" fn export_vault_state_ptr() -> *const u8 {
EXPORT_BUFFER.with(|b| b.borrow().as_ptr())
}
#[no_mangle]
#[cfg(feature = "fhe-state")]
pub extern "C" fn import_vault_state(state_bytes_ptr: *const u8, len: usize) -> u32 {
let state_bytes = unsafe { core::slice::from_raw_parts(state_bytes_ptr, len) };
if state_bytes.is_empty() || state_bytes[0] != VAULT_STATE_VERSION {
return ERR_DESER;
}
let new_state: VaultState = match bincode::deserialize(&state_bytes[1..]) {
Ok(s) => s,
Err(_) => return ERR_DESER,
};
STATE.with(|s| {
*s.borrow_mut() = Some(new_state);
});
ERR_OK
}
#[no_mangle]
#[cfg(not(feature = "fhe-state"))]
pub extern "C" fn import_vault_state(_state_bytes_ptr: *const u8, _len: usize) -> u32 {
ERR_DESER
}
#[cfg(feature = "fhe-state")]
pub fn vault_init_from_bytes(server_key_bytes: &[u8]) -> u32 {
init_vault(server_key_bytes.as_ptr(), server_key_bytes.len())
}
#[cfg(feature = "fhe-state")]
pub fn vault_register_from_bytes(vault_id: &[u8], initial_balance_ct: &[u8]) -> u32 {
register_vault_ciphertext(
vault_id.as_ptr(),
vault_id.len(),
initial_balance_ct.as_ptr(),
initial_balance_ct.len(),
)
}
#[cfg(feature = "fhe-state")]
pub fn vault_transfer_from_bytes(sender_id: &[u8], receiver_id: &[u8], transfer_ct: &[u8]) -> u32 {
homomorphic_transfer(
sender_id.as_ptr(),
sender_id.len(),
receiver_id.as_ptr(),
receiver_id.len(),
transfer_ct.as_ptr(),
transfer_ct.len(),
)
}
#[cfg(feature = "fhe-state")]
pub fn vault_get_balance(vault_id: &[u8]) -> Option<Vec<u8>> {
STATE.with(|s| {
let borrow = s.borrow();
let state = borrow.as_ref()?;
state
.balances
.iter()
.find(|(id, _)| id.as_slice() == vault_id)
.map(|(_, ct)| ct.clone())
})
}
#[cfg(feature = "fhe-state")]
pub fn register_vault_with_identity(
projection_bytes: &[u8],
initial_balance_ct: &[u8],
) -> Result<[u8; 32], u32> {
if aethel_core::EphemeralProjection::from_bytes(projection_bytes).is_err() {
return Err(ERR_DESER);
}
let vault_id = derive_vault_id(projection_bytes);
with_state_mut(|state| {
state.balances.push((vault_id, initial_balance_ct.to_vec()));
state
.identity_projections
.push((vault_id, projection_bytes.to_vec()));
});
Ok(vault_id)
}
#[cfg(feature = "fhe-state")]
pub fn homomorphic_transfer_authenticated(
sender_id: &[u8; 32],
receiver_id: &[u8; 32],
transfer_ct: &[u8],
sender_projection: &aethel_core::EphemeralProjection,
proof: &aethel_core::ZkIdentityProof,
) -> u32 {
let bound_projection_bytes: Option<Vec<u8>> = STATE.with(|s| {
let borrow = s.borrow();
borrow.as_ref().and_then(|state| {
state
.identity_projections
.iter()
.find(|(id, _)| id == sender_id)
.map(|(_, bytes)| bytes.clone())
})
});
let bound_projection_bytes = match bound_projection_bytes {
Some(bytes) => bytes,
None => return ERR_UNAUTHORIZED,
};
if bound_projection_bytes != sender_projection.to_bytes() {
return ERR_UNAUTHORIZED;
}
if !aethel_core::Verifier::verify(sender_projection, proof) {
return ERR_UNAUTHORIZED;
}
vault_transfer_from_bytes(sender_id, receiver_id, transfer_ct)
}
#[cfg(feature = "fhe-state")]
pub fn homomorphic_transfer_authenticated_bytes(
sender_id: &[u8; 32],
receiver_id: &[u8; 32],
transfer_ct: &[u8],
projection_bytes: &[u8],
proof_bytes: &[u8],
context: &[u8],
) -> u32 {
let bound_projection_bytes: Option<Vec<u8>> = STATE.with(|s| {
let borrow = s.borrow();
borrow.as_ref().and_then(|state| {
state
.identity_projections
.iter()
.find(|(id, _)| id == sender_id)
.map(|(_, bytes)| bytes.clone())
})
});
let bound_projection_bytes = match bound_projection_bytes {
Some(bytes) => bytes,
None => return ERR_UNAUTHORIZED,
};
let decoded_projection = match aethel_core::wire::decode_projection(projection_bytes) {
Ok(p) => p,
Err(_) => return ERR_WIRE_VERIFY_FAILED,
};
if decoded_projection.to_bytes() != bound_projection_bytes {
return ERR_UNAUTHORIZED;
}
match aethel_core::wire::verify_projection(projection_bytes, proof_bytes, context) {
Ok(true) => {}
Ok(false) | Err(_) => return ERR_WIRE_VERIFY_FAILED,
}
vault_transfer_from_bytes(sender_id, receiver_id, transfer_ct)
}
#[cfg(feature = "fhe-state")]
pub fn vault_export_state() -> Vec<u8> {
export_vault_state_inner()
}
#[cfg(feature = "fhe-state")]
pub fn vault_import_state(state_bytes: &[u8]) -> u32 {
import_vault_state(state_bytes.as_ptr(), state_bytes.len())
}
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn derive_vault_id_from_projection(projection_bytes: &[u8]) -> Vec<u8> {
derive_vault_id(projection_bytes).to_vec()
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_init(server_key_bytes: &[u8]) -> u32 {
vault_init_from_bytes(server_key_bytes)
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_register(vault_id: &[u8], initial_balance_ct: &[u8]) -> u32 {
vault_register_from_bytes(vault_id, initial_balance_ct)
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_transfer(sender_id: &[u8], receiver_id: &[u8], transfer_ct: &[u8]) -> u32 {
vault_transfer_from_bytes(sender_id, receiver_id, transfer_ct)
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_transfer_authenticated_bytes(
sender_id: &[u8],
receiver_id: &[u8],
transfer_ct: &[u8],
projection_bytes: &[u8],
proof_bytes: &[u8],
context: &[u8],
) -> u32 {
if sender_id.len() != 32 || receiver_id.len() != 32 {
return ERR_DESER;
}
let mut sender = [0u8; 32];
let mut receiver = [0u8; 32];
sender.copy_from_slice(sender_id);
receiver.copy_from_slice(receiver_id);
homomorphic_transfer_authenticated_bytes(
&sender,
&receiver,
transfer_ct,
projection_bytes,
proof_bytes,
context,
)
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_register_with_identity(
projection_bytes: &[u8],
initial_balance_ct: &[u8],
) -> Vec<u8> {
register_vault_with_identity(projection_bytes, initial_balance_ct)
.map(|id| id.to_vec())
.unwrap_or_default()
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_export_state() -> Vec<u8> {
vault_export_state()
}
#[cfg(all(feature = "wasm", feature = "fhe-state"))]
#[wasm_bindgen]
pub fn wasm_vault_import_state(state_bytes: &[u8]) -> u32 {
vault_import_state(state_bytes)
}
#[cfg(all(test, feature = "fhe-state"))]
mod custody_tests {
use super::*;
#[test]
fn exported_state_does_not_contain_server_key() {
let marker: &[u8] = b"\xDE\xAD\xBE\xEF-SERVER-KEY-MARKER-\xDE\xAD\xBE\xEF";
RUNTIME_KEYS.with(|k| {
k.borrow_mut().server_key_bytes = Some(marker.to_vec());
});
let vault_id = [9u8; 32];
with_state_mut(|state| {
state.balances.push((vault_id, alloc::vec![1, 2, 3]));
});
let exported = export_vault_state_inner();
assert!(
!exported.windows(marker.len()).any(|w| w == marker),
"the exported vault state contains the ServerKey marker bytes"
);
RUNTIME_KEYS.with(|k| *k.borrow_mut() = RuntimeKeys::default());
STATE.with(|s| *s.borrow_mut() = None);
assert_eq!(vault_import_state(&exported), ERR_OK);
#[cfg(not(target_arch = "wasm32"))]
{
let receiver_id = [10u8; 32];
vault_register_from_bytes(&receiver_id, &alloc::vec![0]);
let result = vault_transfer_from_bytes(&vault_id, &receiver_id, &alloc::vec![0]);
assert_eq!(
result, ERR_INVALID_KEY,
"a transfer succeeded after import_vault_state with no init_vault call"
);
}
}
#[test]
fn import_rejects_a_payload_with_the_wrong_version_byte() {
let vault_id = [3u8; 32];
with_state_mut(|state| {
state.balances.push((vault_id, alloc::vec![7]));
});
let mut exported = export_vault_state_inner();
assert_eq!(exported[0], VAULT_STATE_VERSION);
exported[0] = 0xFF;
assert_eq!(vault_import_state(&exported), ERR_DESER);
}
#[test]
fn import_rejects_an_empty_payload() {
assert_eq!(vault_import_state(&[]), ERR_DESER);
}
#[test]
fn export_import_round_trips_the_ledger_content() {
let vault_id = [5u8; 32];
with_state_mut(|state| {
state.balances.push((vault_id, alloc::vec![42, 43]));
});
let exported = export_vault_state_inner();
STATE.with(|s| *s.borrow_mut() = None);
assert_eq!(vault_import_state(&exported), ERR_OK);
let balance = vault_get_balance(&vault_id).expect("vault must still exist");
assert_eq!(balance, alloc::vec![42, 43]);
}
}