use noah_algebra::{
collections::HashMap,
prelude::*,
ristretto::{CompressedRistretto, PedersenCommitmentRistretto, RistrettoScalar},
traits::PedersenCommitment,
};
use serde::ser::Serialize;
pub mod asset_mixer;
pub mod asset_record;
pub mod asset_tracer;
pub mod proofs;
pub mod structs;
#[cfg(test)]
pub(crate) mod tests;
use crate::anon_creds::{ACCommitment, Attr};
use crate::keys::{KeyPair, MultiSig, PublicKey};
use crate::setup::BulletproofParams;
use self::{
asset_mixer::{
batch_verify_asset_mixing, prove_asset_mixing, AssetMixProof, AssetMixingInstance,
},
proofs::{
asset_amount_tracing_proofs, asset_proof, batch_verify_confidential_amount,
batch_verify_confidential_asset, batch_verify_tracer_tracing_proof, gen_range_proof,
},
structs::*,
};
const POW_2_32: u64 = 0xFFFF_FFFFu64 + 1;
#[derive(Clone, Copy, Debug)]
#[allow(non_camel_case_types)]
#[allow(clippy::enum_variant_names)]
pub(super) enum XfrType {
NonConfidential_SingleAsset,
ConfidentialAmount_NonConfidentialAssetType_SingleAsset,
NonConfidentialAmount_ConfidentialAssetType_SingleAsset,
Confidential_SingleAsset,
Confidential_MultiAsset,
NonConfidential_MultiAsset,
}
impl XfrType {
pub(super) fn from_inputs_outputs(
inputs_record: &[AssetRecord],
outputs_record: &[AssetRecord],
) -> Self {
let mut multi_asset = false;
let mut confidential_amount_nonconfidential_asset_type = false;
let mut confidential_asset_type_nonconfidential_amount = false;
let mut confidential_all = false;
let asset_type = inputs_record[0].open_asset_record.asset_type;
for record in inputs_record.iter().chain(outputs_record) {
if asset_type != record.open_asset_record.asset_type {
multi_asset = true;
}
let confidential_amount = matches!(
record.open_asset_record.blind_asset_record.amount,
XfrAmount::Confidential(_)
);
let confidential_asset_type = matches!(
record.open_asset_record.blind_asset_record.asset_type,
XfrAssetType::Confidential(_)
);
if confidential_amount && confidential_asset_type {
confidential_all = true;
} else if confidential_amount {
confidential_amount_nonconfidential_asset_type = true;
} else if confidential_asset_type {
confidential_asset_type_nonconfidential_amount = true;
}
}
if multi_asset {
if confidential_all
|| confidential_amount_nonconfidential_asset_type
|| confidential_asset_type_nonconfidential_amount
{
return XfrType::Confidential_MultiAsset;
} else {
return XfrType::NonConfidential_MultiAsset;
}
}
if confidential_all
|| (confidential_amount_nonconfidential_asset_type
&& confidential_asset_type_nonconfidential_amount)
{
XfrType::Confidential_SingleAsset
} else if confidential_amount_nonconfidential_asset_type {
XfrType::ConfidentialAmount_NonConfidentialAssetType_SingleAsset
} else if confidential_asset_type_nonconfidential_amount {
XfrType::NonConfidentialAmount_ConfidentialAssetType_SingleAsset
} else {
XfrType::NonConfidential_SingleAsset
}
}
}
pub fn gen_xfr_note<R: CryptoRng + RngCore>(
prng: &mut R,
inputs: &[AssetRecord],
outputs: &[AssetRecord],
input_key_pairs: &[&KeyPair],
) -> Result<XfrNote> {
if inputs.is_empty() {
return Err(eg!(NoahError::ParameterError));
}
check_keys(inputs, input_key_pairs).c(d!())?;
let body = gen_xfr_body(prng, inputs, outputs).c(d!())?;
let multisig = compute_transfer_multisig(&body, input_key_pairs).c(d!())?;
Ok(XfrNote { body, multisig })
}
pub fn gen_xfr_body<R: CryptoRng + RngCore>(
prng: &mut R,
inputs: &[AssetRecord],
outputs: &[AssetRecord],
) -> Result<XfrBody> {
if inputs.is_empty() {
return Err(eg!(NoahError::ParameterError));
}
let xfr_type = XfrType::from_inputs_outputs(inputs, outputs);
check_asset_amount(inputs, outputs).c(d!())?;
let single_asset = !matches!(
xfr_type,
XfrType::NonConfidential_MultiAsset | XfrType::Confidential_MultiAsset
);
let open_inputs = inputs
.iter()
.map(|input| &input.open_asset_record)
.collect_vec();
let open_outputs = outputs
.iter()
.map(|output| &output.open_asset_record)
.collect_vec();
let asset_amount_proof = if single_asset {
gen_xfr_proofs_single_asset(
prng,
open_inputs.as_slice(),
open_outputs.as_slice(),
xfr_type,
)
.c(d!())?
} else {
gen_xfr_proofs_multi_asset(open_inputs.as_slice(), open_outputs.as_slice(), xfr_type)
.c(d!())?
};
let asset_type_amount_tracing_proof =
asset_amount_tracing_proofs(prng, inputs, outputs).c(d!())?;
let asset_tracing_proof = AssetTracingProofs {
asset_type_and_amount_proofs: asset_type_amount_tracing_proof,
inputs_identity_proofs: inputs
.iter()
.map(|input| input.identity_proofs.clone())
.collect_vec(),
outputs_identity_proofs: outputs
.iter()
.map(|output| output.identity_proofs.clone())
.collect_vec(),
};
let proofs = XfrProofs {
asset_type_and_amount_proof: asset_amount_proof,
asset_tracing_proof,
};
let mut xfr_inputs = vec![];
for x in open_inputs {
xfr_inputs.push(x.blind_asset_record.clone())
}
let mut xfr_outputs = vec![];
for x in open_outputs {
xfr_outputs.push(x.blind_asset_record.clone())
}
let tracer_memos = inputs
.iter()
.chain(outputs)
.map(|record_input| record_input.asset_tracers_memos.clone())
.collect_vec();
let owner_memos = outputs
.iter()
.map(|record_input| record_input.owner_memo.clone())
.collect_vec();
Ok(XfrBody {
inputs: xfr_inputs,
outputs: xfr_outputs,
proofs,
asset_tracing_memos: tracer_memos,
owners_memos: owner_memos,
})
}
fn check_keys(inputs: &[AssetRecord], input_key_pairs: &[&KeyPair]) -> Result<()> {
if inputs.len() != input_key_pairs.len() {
return Err(eg!(NoahError::ParameterError));
}
for (input, key) in inputs.iter().zip(input_key_pairs.iter()) {
let inkey = &input.open_asset_record.blind_asset_record.public_key;
if inkey != &key.pub_key {
return Err(eg!(NoahError::ParameterError));
}
}
Ok(())
}
fn gen_xfr_proofs_multi_asset(
inputs: &[&OpenAssetRecord],
outputs: &[&OpenAssetRecord],
xfr_type: XfrType,
) -> Result<AssetTypeAndAmountProof> {
let pow2_32 = RistrettoScalar::from(POW_2_32);
let mut ins = vec![];
for x in inputs.iter() {
ins.push((
x.amount,
x.asset_type.as_scalar(),
x.amount_blinds.0.add(&pow2_32.mul(&x.amount_blinds.1)),
x.type_blind,
));
}
let mut out = vec![];
for x in outputs.iter() {
out.push((
x.amount,
x.asset_type.as_scalar(),
x.amount_blinds.0.add(&pow2_32.mul(&x.amount_blinds.1)),
x.type_blind,
));
}
match xfr_type {
XfrType::Confidential_MultiAsset => {
let mix_proof = prove_asset_mixing(ins.as_slice(), out.as_slice()).c(d!())?;
Ok(AssetTypeAndAmountProof::AssetMix(mix_proof))
}
XfrType::NonConfidential_MultiAsset => Ok(AssetTypeAndAmountProof::NoProof),
_ => Err(eg!(NoahError::XfrCreationAssetAmountError)),
}
}
fn gen_xfr_proofs_single_asset<R: CryptoRng + RngCore>(
prng: &mut R,
inputs: &[&OpenAssetRecord],
outputs: &[&OpenAssetRecord],
xfr_type: XfrType,
) -> Result<AssetTypeAndAmountProof> {
let pc_gens = PedersenCommitmentRistretto::default();
match xfr_type {
XfrType::NonConfidential_SingleAsset => Ok(AssetTypeAndAmountProof::NoProof),
XfrType::ConfidentialAmount_NonConfidentialAssetType_SingleAsset => Ok(
AssetTypeAndAmountProof::ConfAmount(gen_range_proof(inputs, outputs).c(d!())?),
),
XfrType::NonConfidentialAmount_ConfidentialAssetType_SingleAsset => {
Ok(AssetTypeAndAmountProof::ConfAsset(Box::new(
asset_proof(prng, &pc_gens, inputs, outputs).c(d!())?,
)))
}
XfrType::Confidential_SingleAsset => Ok(AssetTypeAndAmountProof::ConfAll(Box::new((
gen_range_proof(inputs, outputs).c(d!())?,
asset_proof(prng, &pc_gens, inputs, outputs).c(d!())?,
)))),
_ => Err(eg!(NoahError::XfrCreationAssetAmountError)), }
}
fn check_asset_amount(inputs: &[AssetRecord], outputs: &[AssetRecord]) -> Result<()> {
let mut amounts = HashMap::new();
for record in inputs.iter() {
match amounts.get_mut(&(record.open_asset_record.asset_type)) {
None => {
amounts.insert(
record.open_asset_record.asset_type,
vec![i128::from(record.open_asset_record.amount)],
);
}
Some(vec) => {
vec.push(i128::from(record.open_asset_record.amount));
}
};
}
for record in outputs.iter() {
match amounts.get_mut(&record.open_asset_record.asset_type) {
None => {
amounts.insert(
record.open_asset_record.asset_type,
vec![-i128::from(record.open_asset_record.amount)],
);
}
Some(vec) => {
vec.push(-i128::from(record.open_asset_record.amount));
}
};
}
for (_, a) in amounts.iter() {
let sum = a.iter().sum::<i128>();
if sum != 0i128 {
return Err(eg!(NoahError::XfrCreationAssetAmountError));
}
}
Ok(())
}
pub(crate) fn compute_transfer_multisig(body: &XfrBody, keys: &[&KeyPair]) -> Result<MultiSig> {
let mut bytes = vec![];
body.serialize(&mut rmp_serde::Serializer::new(&mut bytes))
.c(d!(NoahError::SerializationError))?;
Ok(MultiSig::sign(&keys, &bytes)?)
}
pub(crate) fn verify_transfer_multisig(xfr_note: &XfrNote) -> Result<()> {
let mut bytes = vec![];
xfr_note
.body
.serialize(&mut rmp_serde::Serializer::new(&mut bytes))
.c(d!(NoahError::SerializationError))?;
let pubkeys = xfr_note
.body
.inputs
.iter()
.map(|input| &input.public_key)
.collect_vec();
xfr_note.multisig.verify(&pubkeys, &bytes)
}
pub fn verify_xfr_note<R: CryptoRng + RngCore>(
prng: &mut R,
params: &mut BulletproofParams,
xfr_note: &XfrNote,
policies: &XfrNotePoliciesRef<'_>,
) -> Result<()> {
batch_verify_xfr_notes(prng, params, &[&xfr_note], &[&policies]).c(d!())
}
pub fn batch_verify_xfr_notes<R: CryptoRng + RngCore>(
prng: &mut R,
params: &mut BulletproofParams,
notes: &[&XfrNote],
policies: &[&XfrNotePoliciesRef<'_>],
) -> Result<()> {
for xfr_note in notes {
verify_transfer_multisig(xfr_note).c(d!())?;
}
let bodies = notes.iter().map(|note| ¬e.body).collect_vec();
batch_verify_xfr_bodies(prng, params, &bodies, policies).c(d!())
}
pub(crate) fn batch_verify_xfr_body_asset_records<R: CryptoRng + RngCore>(
prng: &mut R,
params: &mut BulletproofParams,
bodies: &[&XfrBody],
) -> Result<()> {
let mut conf_amount_records = vec![];
let mut conf_asset_type_records = vec![];
let mut conf_asset_mix_bodies = vec![];
for body in bodies {
match &body.proofs.asset_type_and_amount_proof {
AssetTypeAndAmountProof::ConfAll(x) => {
let range_proof = &(*x).0;
let asset_proof = &(*x).1;
conf_amount_records.push((&body.inputs, &body.outputs, range_proof));
conf_asset_type_records.push((&body.inputs, &body.outputs, asset_proof));
}
AssetTypeAndAmountProof::ConfAmount(range_proof) => {
conf_amount_records.push((&body.inputs, &body.outputs, range_proof)); verify_plain_asset(body.inputs.as_slice(), body.outputs.as_slice()).c(d!())?;
}
AssetTypeAndAmountProof::ConfAsset(asset_proof) => {
verify_plain_amounts(body.inputs.as_slice(), body.outputs.as_slice()).c(d!())?; conf_asset_type_records.push((&body.inputs, &body.outputs, asset_proof));
}
AssetTypeAndAmountProof::NoProof => {
verify_plain_asset_mix(body.inputs.as_slice(), body.outputs.as_slice()).c(d!())?;
}
AssetTypeAndAmountProof::AssetMix(asset_mix_proof) => {
conf_asset_mix_bodies.push((
body.inputs.as_slice(),
body.outputs.as_slice(),
asset_mix_proof,
));
}
}
}
batch_verify_confidential_amount(prng, params, conf_amount_records.as_slice()).c(d!())?;
batch_verify_confidential_asset(prng, &conf_asset_type_records).c(d!())?;
batch_verify_asset_mix(prng, params, conf_asset_mix_bodies.as_slice()).c(d!())
}
#[derive(Clone, Default)]
pub struct XfrNotePoliciesRef<'b> {
pub(crate) valid: bool,
pub(crate) inputs_tracing_policies: Vec<&'b TracingPolicies>,
pub(crate) inputs_sig_commitments: Vec<Option<&'b ACCommitment>>,
pub(crate) outputs_tracing_policies: Vec<&'b TracingPolicies>,
pub(crate) outputs_sig_commitments: Vec<Option<&'b ACCommitment>>,
}
impl<'b> XfrNotePoliciesRef<'b> {
pub fn new(
inputs_tracing_policies: Vec<&'b TracingPolicies>,
inputs_sig_commitments: Vec<Option<&'b ACCommitment>>,
outputs_tracing_policies: Vec<&'b TracingPolicies>,
outputs_sig_commitments: Vec<Option<&'b ACCommitment>>,
) -> XfrNotePoliciesRef<'b> {
XfrNotePoliciesRef {
valid: true,
inputs_tracing_policies,
inputs_sig_commitments,
outputs_tracing_policies,
outputs_sig_commitments,
}
}
}
pub(crate) fn if_some_closure(x: &Option<ACCommitment>) -> Option<&ACCommitment> {
if (*x).is_some() {
Some(x.as_ref().unwrap()) } else {
None
}
}
#[derive(Clone, Default, Serialize, Deserialize, Eq, PartialEq, Debug)]
pub struct XfrNotePolicies {
pub valid: bool, pub inputs_tracing_policies: Vec<TracingPolicies>,
pub inputs_sig_commitments: Vec<Option<ACCommitment>>,
pub outputs_tracing_policies: Vec<TracingPolicies>,
pub outputs_sig_commitments: Vec<Option<ACCommitment>>,
}
impl XfrNotePolicies {
pub fn new(
inputs_tracing_policies: Vec<TracingPolicies>,
inputs_sig_commitments: Vec<Option<ACCommitment>>,
outputs_tracing_policies: Vec<TracingPolicies>,
outputs_sig_commitments: Vec<Option<ACCommitment>>,
) -> XfrNotePolicies {
XfrNotePolicies {
valid: true,
inputs_tracing_policies,
inputs_sig_commitments,
outputs_tracing_policies,
outputs_sig_commitments,
}
}
pub fn empty_policies(num_inputs: usize, num_outputs: usize) -> XfrNotePolicies {
XfrNotePolicies {
valid: true,
inputs_tracing_policies: vec![Default::default(); num_inputs],
inputs_sig_commitments: vec![None; num_inputs],
outputs_tracing_policies: vec![Default::default(); num_outputs],
outputs_sig_commitments: vec![None; num_outputs],
}
}
pub fn to_ref(&self) -> XfrNotePoliciesRef<'_> {
if self.valid {
XfrNotePoliciesRef::new(
self.inputs_tracing_policies.iter().collect_vec(),
self.inputs_sig_commitments
.iter()
.map(if_some_closure)
.collect_vec(),
self.outputs_tracing_policies.iter().collect_vec(),
self.outputs_sig_commitments
.iter()
.map(if_some_closure)
.collect_vec(),
)
} else {
XfrNotePoliciesRef::default()
}
}
}
pub fn verify_xfr_body<R: CryptoRng + RngCore>(
prng: &mut R,
params: &mut BulletproofParams,
body: &XfrBody,
policies: &XfrNotePoliciesRef<'_>,
) -> Result<()> {
batch_verify_xfr_bodies(prng, params, &[body], &[policies]).c(d!())
}
pub fn batch_verify_xfr_bodies<R: CryptoRng + RngCore>(
prng: &mut R,
params: &mut BulletproofParams,
bodies: &[&XfrBody],
policies: &[&XfrNotePoliciesRef<'_>],
) -> Result<()> {
batch_verify_xfr_body_asset_records(prng, params, bodies).c(d!())?;
batch_verify_tracer_tracing_proof(prng, bodies, policies).c(d!())
}
fn safe_sum_u64(terms: &[u64]) -> u128 {
terms.iter().map(|x| u128::from(*x)).sum()
}
fn verify_plain_amounts(inputs: &[BlindAssetRecord], outputs: &[BlindAssetRecord]) -> Result<()> {
let in_amount: Result<Vec<u64>> = inputs
.iter()
.map(|x| x.amount.get_amount().c(d!(NoahError::ParameterError)))
.collect();
let out_amount: Result<Vec<u64>> = outputs
.iter()
.map(|x| x.amount.get_amount().c(d!(NoahError::ParameterError)))
.collect();
let sum_inputs = safe_sum_u64(in_amount.c(d!())?.as_slice());
let sum_outputs = safe_sum_u64(out_amount.c(d!())?.as_slice());
if sum_inputs != sum_outputs {
return Err(eg!(NoahError::XfrVerifyAssetAmountError));
}
Ok(())
}
fn verify_plain_asset(inputs: &[BlindAssetRecord], outputs: &[BlindAssetRecord]) -> Result<()> {
let mut list = vec![];
for x in inputs.iter() {
list.push(
x.asset_type
.get_asset_type()
.c(d!(NoahError::ParameterError))?,
);
}
for x in outputs.iter() {
list.push(
x.asset_type
.get_asset_type()
.c(d!(NoahError::ParameterError))?,
);
}
if list.iter().all_equal() {
Ok(())
} else {
Err(eg!(NoahError::XfrVerifyAssetAmountError))
}
}
fn verify_plain_asset_mix(inputs: &[BlindAssetRecord], outputs: &[BlindAssetRecord]) -> Result<()> {
let mut amounts = HashMap::new();
for record in inputs.iter() {
match amounts.get_mut(
&record
.asset_type
.get_asset_type()
.c(d!(NoahError::ParameterError))?,
) {
None => {
amounts.insert(
record
.asset_type
.get_asset_type()
.c(d!(NoahError::ParameterError))?,
vec![i128::from(
record
.amount
.get_amount()
.c(d!(NoahError::ParameterError))?,
)],
);
}
Some(vec) => {
vec.push(i128::from(
record
.amount
.get_amount()
.c(d!(NoahError::ParameterError))?,
));
}
};
}
for record in outputs.iter() {
match amounts.get_mut(
&record
.asset_type
.get_asset_type()
.c(d!(NoahError::ParameterError))?,
) {
None => {
amounts.insert(
record
.asset_type
.get_asset_type()
.c(d!(NoahError::ParameterError))?,
vec![-i128::from(
record
.amount
.get_amount()
.c(d!(NoahError::ParameterError))?,
)],
);
}
Some(vec) => {
vec.push(-i128::from(
record
.amount
.get_amount()
.c(d!(NoahError::ParameterError))?,
));
}
};
}
for (_, a) in amounts.iter() {
let sum = a.iter().sum::<i128>();
if sum != 0i128 {
return Err(eg!(NoahError::XfrVerifyAssetAmountError));
}
}
Ok(())
}
fn batch_verify_asset_mix<R: CryptoRng + RngCore>(
prng: &mut R,
params: &mut BulletproofParams,
bars_instances: &[(&[BlindAssetRecord], &[BlindAssetRecord], &AssetMixProof)],
) -> Result<()> {
fn process_bars(
bars: &[BlindAssetRecord],
) -> Result<Vec<(CompressedRistretto, CompressedRistretto)>> {
let pow2_32 = RistrettoScalar::from(POW_2_32);
bars.iter()
.map(|x| {
let (com_amount_low, com_amount_high) = match x.amount {
XfrAmount::Confidential((c1, c2)) => (
c1.decompress().c(d!(NoahError::DecompressElementError)),
c2.decompress().c(d!(NoahError::DecompressElementError)),
),
XfrAmount::NonConfidential(amount) => {
let pc_gens = PedersenCommitmentRistretto::default();
let (low, high) = u64_to_u32_pair(amount);
(
Ok(pc_gens.commit(RistrettoScalar::from(low), RistrettoScalar::zero())),
Ok(pc_gens
.commit(RistrettoScalar::from(high), RistrettoScalar::zero())),
)
}
};
match (com_amount_low, com_amount_high) {
(Ok(com_amount_low), Ok(com_amount_high)) => {
let com_amount =
(com_amount_low.add(&com_amount_high.mul(&pow2_32))).compress();
let com_type = match x.asset_type {
XfrAssetType::Confidential(c) => c,
XfrAssetType::NonConfidential(asset_type) => {
let pc_gens = PedersenCommitmentRistretto::default();
pc_gens
.commit(asset_type.as_scalar(), RistrettoScalar::zero())
.compress()
}
};
Ok((com_amount, com_type))
}
_ => Err(eg!(NoahError::ParameterError)),
}
})
.collect()
}
let mut asset_mix_instances = vec![];
for instance in bars_instances {
let in_coms = process_bars(instance.0).c(d!())?;
let out_coms = process_bars(instance.1).c(d!())?;
asset_mix_instances.push(AssetMixingInstance {
inputs: in_coms,
outputs: out_coms,
proof: instance.2,
});
}
batch_verify_asset_mixing(prng, params, &asset_mix_instances).c(d!())
}
pub fn find_tracing_memos<'a>(
xfr_body: &'a XfrBody,
pub_key: &AssetTracerEncKeys,
) -> Result<Vec<(&'a BlindAssetRecord, &'a TracerMemo)>> {
let mut result = vec![];
if xfr_body.inputs.len() + xfr_body.outputs.len() != xfr_body.asset_tracing_memos.len() {
return Err(eg!(NoahError::InconsistentStructureError));
}
for (blind_asset_record, bar_memos) in xfr_body
.inputs
.iter()
.chain(&xfr_body.outputs)
.zip(&xfr_body.asset_tracing_memos)
{
for memo in bar_memos {
if memo.enc_key == *pub_key {
result.push((blind_asset_record, memo));
}
}
}
Ok(result)
}
pub type RecordData = (u64, AssetType, Vec<Attr>, PublicKey);
pub fn trace_assets(
xfr_body: &XfrBody,
tracer_keypair: &AssetTracerKeyPair,
) -> Result<Vec<RecordData>> {
let bars_memos = find_tracing_memos(xfr_body, &tracer_keypair.enc_key).c(d!())?;
extract_tracing_info(bars_memos.as_slice(), &tracer_keypair.dec_key).c(d!())
}
pub(crate) fn extract_tracing_info(
memos: &[(&BlindAssetRecord, &TracerMemo)],
dec_key: &AssetTracerDecKeys,
) -> Result<Vec<RecordData>> {
let mut result = vec![];
for (blind_asset_record, memo) in memos {
let (amount_option, asset_type_option, attributes) = memo.decrypt(dec_key).c(d!())?; let amount = match memo.lock_amount {
None => blind_asset_record
.amount
.get_amount()
.c(d!(NoahError::InconsistentStructureError))?,
Some(_) => match amount_option {
None => {
return Err(eg!(NoahError::InconsistentStructureError));
}
Some(amt) => amt,
},
};
let asset_type = match memo.lock_asset_type {
None => blind_asset_record
.asset_type
.get_asset_type()
.c(d!(NoahError::InconsistentStructureError))?,
Some(_) => match asset_type_option {
None => {
return Err(eg!(NoahError::InconsistentStructureError));
}
Some(asset_type) => asset_type,
},
};
result.push((
amount,
asset_type,
attributes,
blind_asset_record.public_key,
));
}
Ok(result)
}