use crate::base::tx::{Transaction, TxOutZarcanum};
use crate::base::types::Value256;
use crate::base::variant::{PAYMENT_ID_SERVICE_ID, TX_SERVICE_ATTACHMENT_ENCRYPT_BODY, Variant};
use crate::crypto::consts::{C_POINT_G, C_POINT_X, NATIVE_COIN_ASSET_ID_PT};
use crate::crypto::{
Point, Scalar, chacha8, chacha8_generate_key, generate_key_derivation, hash_to_scalar, mul8,
scalar_int,
};
use crate::error::{Error, Result};
use crate::txdest::{
CRYPTO_HDS_OUT_AMOUNT_BLINDING_MASK, CRYPTO_HDS_OUT_AMOUNT_MASK,
CRYPTO_HDS_OUT_ASSET_BLIND_MASK, CRYPTO_HDS_OUT_CONCEALING_POINT, hs_domain,
};
use crate::wallet::Wallet;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ReceivedOutput {
pub output_index: usize,
pub amount: u64,
pub asset_id: Value256,
pub is_native: bool,
pub stealth_address: Value256,
#[serde(with = "hex_scalar")]
pub amount_blinding_mask: Scalar,
#[serde(with = "hex_scalar")]
pub asset_id_blinding_mask: Scalar,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ScanResult {
pub tx_pub_key: Value256,
pub outputs: Vec<ReceivedOutput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub payment_id: Option<Vec<u8>>,
}
impl ScanResult {
pub fn found(&self) -> bool {
!self.outputs.is_empty()
}
}
mod hex_scalar {
use crate::crypto::Scalar;
use serde::Serializer;
use serde::de::{Deserialize, Deserializer, Error as _};
pub fn serialize<S: Serializer>(v: &Scalar, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&hex::encode(v.to_bytes()))
}
pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Scalar, D::Error> {
let s = String::deserialize(d)?;
let b = hex::decode(&s).map_err(D::Error::custom)?;
let arr: [u8; 32] = b
.as_slice()
.try_into()
.map_err(|_| D::Error::custom("scalar must be 32 bytes"))?;
Scalar::from_bytes_canonical(&arr).ok_or_else(|| D::Error::custom("non-canonical scalar"))
}
}
impl Wallet {
pub fn scan_tx(&self, tx: &Transaction) -> Result<ScanResult> {
let mut res = ScanResult::default();
let Some(tx_pub) = tx.tx_pub_key() else {
return Ok(res); };
res.tx_pub_key = tx_pub;
let tx_pub_pt = tx_pub
.to_point()
.ok_or_else(|| Error::msg("scan: invalid tx public key in extra"))?;
let derivation = generate_key_derivation(&tx_pub_pt, &self.view_priv_key);
let derivation_bytes = derivation.compress();
for (i, v) in tx.vout.iter().enumerate() {
let Some(zo) = v.as_tx_out_zarcanum() else {
continue;
};
let mut buf = derivation_bytes.to_vec();
crate::base::varint::append_varint(&mut buf, i as u64);
let h = hash_to_scalar(&buf);
let exp_stealth = Point::mul_base(&h).add(&self.spend_pub_key);
if exp_stealth.compress() != zo.stealth_address.0 {
continue;
}
res.outputs.push(self.decode_output(i, zo, &h)?);
}
res.payment_id = self.recover_payment_id(tx, &derivation_bytes);
Ok(res)
}
fn decode_output(&self, i: usize, zo: &TxOutZarcanum, h: &Scalar) -> Result<ReceivedOutput> {
let amount_mask = hs_domain(CRYPTO_HDS_OUT_AMOUNT_MASK, h);
let mut mask8 = [0u8; 8];
mask8.copy_from_slice(&amount_mask.to_bytes()[..8]);
let amount = zo.encrypted_amount ^ u64::from_le_bytes(mask8);
let amount_blinding_mask = hs_domain(CRYPTO_HDS_OUT_AMOUNT_BLINDING_MASK, h);
let blinded_asset_id_pt = zo
.blinded_asset_id
.to_point()
.ok_or_else(|| Error::msg("scan: invalid blinded_asset_id"))?;
let blinded_asset_full = mul8(&blinded_asset_id_pt);
let a_prime = blinded_asset_full
.mul(&scalar_int(amount))
.add(&C_POINT_G.mul(&amount_blinding_mask));
let amount_commitment_pt = zo
.amount_commitment
.to_point()
.ok_or_else(|| Error::msg("scan: invalid amount_commitment"))?;
if a_prime != mul8(&amount_commitment_pt) {
return Err(Error::msg(
"scan: amount commitment mismatch for owned output (malformed tx?)",
));
}
let h_q = hs_domain(CRYPTO_HDS_OUT_CONCEALING_POINT, h);
let q_prime = self.view_pub_key.mul(&h_q);
if q_prime.compress() != zo.concealing_point.0 {
return Err(Error::msg(
"scan: concealing point mismatch for owned output (malformed tx?)",
));
}
let (is_native, asset_id, asset_id_blinding_mask) =
if blinded_asset_full == *NATIVE_COIN_ASSET_ID_PT {
(
true,
Value256::from_point(&NATIVE_COIN_ASSET_ID_PT),
Scalar::ZERO,
)
} else {
let mask = hs_domain(CRYPTO_HDS_OUT_ASSET_BLIND_MASK, h);
let asset_pt = blinded_asset_full.sub(&C_POINT_X.mul(&mask));
(
asset_pt == *NATIVE_COIN_ASSET_ID_PT,
Value256::from_point(&asset_pt),
mask,
)
};
Ok(ReceivedOutput {
output_index: i,
amount,
asset_id,
is_native,
stealth_address: zo.stealth_address,
amount_blinding_mask,
asset_id_blinding_mask,
})
}
fn recover_payment_id(&self, tx: &Transaction, derivation_bytes: &[u8]) -> Option<Vec<u8>> {
let find = |items: &[Variant]| -> Option<crate::base::variant::TxServiceAttachment> {
items
.iter()
.filter_map(|e| e.as_service_attachment())
.find(|sa| sa.service_id == PAYMENT_ID_SERVICE_ID)
.cloned()
};
let sa = find(&tx.extra).or_else(|| find(&tx.attachment))?;
let mut body = sa.body.clone();
if sa.flags & TX_SERVICE_ATTACHMENT_ENCRYPT_BODY != 0 {
let code = chacha8_generate_key(derivation_bytes).ok()?;
body = chacha8(&code, &[0u8; 8], &body).ok()?;
}
Some(body)
}
}