use std::num::NonZeroU64;
use indexmap::IndexSet;
use tari_crypto::ristretto::RistrettoPublicKey;
use tari_ootle_common_types::engine_types::crypto::OutputBody;
use tari_ootle_wallet_crypto::{memo::Memo, pay_to::PayTo};
use tari_template_lib_types::{
Amount,
EncryptedData,
ResourceAddress,
crypto::{PedersenCommitmentBytes, UtxoTag},
stealth::{SpendCondition, StealthInput, TemplateFunction},
};
use crate::Address;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct StealthSignerRequirement {
signer: Address,
public_nonce: RistrettoPublicKey,
}
impl StealthSignerRequirement {
pub fn new(signer: Address, public_nonce: RistrettoPublicKey) -> Self {
Self { signer, public_nonce }
}
pub fn signer(&self) -> &Address {
&self.signer
}
pub fn public_nonce(&self) -> &RistrettoPublicKey {
&self.public_nonce
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SealSource {
AccountKey,
StealthInput(StealthSignerRequirement),
Ephemeral,
}
impl SealSource {
fn take_precedence_over(self, other: Self) -> (Self, Option<StealthSignerRequirement>) {
match (self, other) {
(Self::AccountKey, Self::StealthInput(displaced)) | (Self::StealthInput(displaced), Self::AccountKey) => {
(Self::AccountKey, Some(displaced))
},
(Self::AccountKey, _) | (_, Self::AccountKey) => (Self::AccountKey, None),
(Self::StealthInput(seal), Self::StealthInput(displaced)) => (Self::StealthInput(seal), Some(displaced)),
(Self::StealthInput(seal), Self::Ephemeral) | (Self::Ephemeral, Self::StealthInput(seal)) => {
(Self::StealthInput(seal), None)
},
(Self::Ephemeral, Self::Ephemeral) => (Self::Ephemeral, None),
}
}
}
#[derive(Debug, Clone)]
pub struct SignatureRequirements {
seal: SealSource,
authorizers: IndexSet<StealthSignerRequirement>,
}
impl SignatureRequirements {
pub fn account_key_seal_with(authorizers: IndexSet<StealthSignerRequirement>) -> Self {
Self {
seal: SealSource::AccountKey,
authorizers,
}
}
pub fn account_key_seal() -> Self {
Self::account_key_seal_with(IndexSet::new())
}
pub fn stealth_seal(signers: IndexSet<StealthSignerRequirement>) -> Self {
let mut signers = signers.into_iter();
match signers.next() {
Some(seal_signer) => Self::stealth_seal_with(seal_signer, signers.collect()),
None => Self {
seal: SealSource::Ephemeral,
authorizers: IndexSet::new(),
},
}
}
pub fn stealth_seal_with(
seal_signer: StealthSignerRequirement,
authorizers: IndexSet<StealthSignerRequirement>,
) -> Self {
Self {
seal: SealSource::StealthInput(seal_signer),
authorizers,
}
}
pub fn seal(&self) -> &SealSource {
&self.seal
}
#[must_use]
pub fn merge(self, other: Self) -> Self {
let (seal, displaced) = self.seal.take_precedence_over(other.seal);
let mut authorizers = self.authorizers;
authorizers.extend(displaced);
authorizers.extend(other.authorizers);
if let SealSource::StealthInput(seal_signer) = &seal {
authorizers.swap_remove(seal_signer);
}
Self { seal, authorizers }
}
pub fn into_parts(self) -> (SealSource, IndexSet<StealthSignerRequirement>) {
(self.seal, self.authorizers)
}
pub fn authorizers(&self) -> impl ExactSizeIterator<Item = &StealthSignerRequirement> {
self.authorizers.iter()
}
}
#[derive(Debug, Clone)]
pub struct Output {
pub destination: Address,
pub amount: NonZeroU64,
pub resource_address: ResourceAddress,
pub resource_view_key: Option<RistrettoPublicKey>,
pub memo: Option<Memo>,
pub pay_to: PayTo,
pub utxo_tag: Option<UtxoTag>,
pub minimum_value_promise: u64,
}
impl Output {
pub fn new(destination: Address, resource_address: ResourceAddress, amount: NonZeroU64) -> Self {
Self {
destination,
amount,
resource_address,
resource_view_key: None,
memo: None,
pay_to: PayTo::default(),
utxo_tag: None,
minimum_value_promise: 0,
}
}
pub fn with_resource_view_key(mut self, resource_view_key: RistrettoPublicKey) -> Self {
self.resource_view_key = Some(resource_view_key);
self
}
pub fn with_memo(mut self, memo: Memo) -> Self {
self.memo = Some(memo);
self
}
pub fn with_memo_message<T: Into<Box<str>>>(self, message: T) -> Self {
self.with_memo(Memo::new_message(message).expect("Memo message too long"))
}
pub fn with_pay_to(mut self, pay_to: PayTo) -> Self {
self.pay_to = pay_to;
self
}
pub fn with_spend_script(self, template_function: TemplateFunction) -> Self {
self.with_pay_to(PayTo::TemplateFunction(template_function))
}
pub fn with_spend_conditions(self, conditions: Vec<SpendCondition>) -> Self {
self.with_pay_to(PayTo::Conditions(conditions))
}
pub fn with_utxo_tag(mut self, utxo_tag: UtxoTag) -> Self {
self.utxo_tag = Some(utxo_tag);
self
}
}
#[derive(Debug, Clone)]
pub struct ResolvedStealthInput {
pub input: StealthInput,
pub output: OutputBody,
}
impl ResolvedStealthInput {
pub fn new(input: StealthInput, output: OutputBody) -> Self {
Self { input, output }
}
pub fn commitment(&self) -> &PedersenCommitmentBytes {
&self.input.commitment
}
}
#[derive(Debug, Clone)]
pub struct ResolvedStealthTransferSpec {
pub inputs: Vec<ResolvedStealthInput>,
pub revealed_input_amount: Amount,
pub outputs: Vec<Output>,
pub revealed_output_amount: Amount,
}
impl ResolvedStealthTransferSpec {
pub fn total_output_amount(&self) -> Amount {
let stealth_output_total: Amount = self.outputs.iter().map(|o| Amount::from(o.amount.get())).sum();
stealth_output_total + self.revealed_output_amount
}
pub fn requires_balance_proof(&self) -> bool {
!self.inputs.is_empty() || !self.outputs.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct BurnClaimStatementSpec {
pub commitment: PedersenCommitmentBytes,
pub encrypted_data: EncryptedData,
pub sender_offset_public_key: RistrettoPublicKey,
pub output: Output,
pub revealed_output_amount: Amount,
}
#[cfg(test)]
mod tests {
use ootle_byte_type::ToByteType;
use tari_crypto::{
keys::{PublicKey, SecretKey},
ristretto::RistrettoSecretKey,
};
use super::*;
use crate::Network;
fn signer_from_seed(seed: u8) -> StealthSignerRequirement {
let secret = RistrettoSecretKey::from_uniform_bytes(&[seed; 64]).unwrap();
let pk = RistrettoPublicKey::from_secret_key(&secret);
let addr = Address::new(Network::LocalNet, pk.to_byte_type(), pk.to_byte_type());
StealthSignerRequirement::new(addr, pk)
}
fn signers<const N: usize>(seeds: [u8; N]) -> IndexSet<StealthSignerRequirement> {
seeds.into_iter().map(signer_from_seed).collect()
}
mod signature_requirement_invariants {
use super::*;
#[test]
fn account_key_seal_authorizes_every_signer() {
let spec = SignatureRequirements::account_key_seal_with(signers([1, 2]));
assert_eq!(spec.seal(), &SealSource::AccountKey);
assert_eq!(spec.authorizers().collect::<Vec<_>>(), vec![
&signer_from_seed(1),
&signer_from_seed(2)
]);
}
#[test]
fn account_key_seal_without_signers_has_no_authorizers() {
let spec = SignatureRequirements::account_key_seal();
assert_eq!(spec.seal(), &SealSource::AccountKey);
assert_eq!(spec.authorizers().len(), 0);
}
#[test]
fn stealth_seal_promotes_the_first_signer() {
let spec = SignatureRequirements::stealth_seal(signers([1, 2, 3]));
assert_eq!(spec.seal(), &SealSource::StealthInput(signer_from_seed(1)));
assert_eq!(spec.authorizers().collect::<Vec<_>>(), vec![
&signer_from_seed(2),
&signer_from_seed(3)
]);
}
#[test]
fn a_single_stealth_signer_seals_and_authorizes_nothing() {
let spec = SignatureRequirements::stealth_seal(signers([1]));
assert_eq!(spec.seal(), &SealSource::StealthInput(signer_from_seed(1)));
assert_eq!(spec.authorizers().len(), 0);
}
#[test]
fn stealth_seal_with_an_explicit_seal_signer() {
let spec = SignatureRequirements::stealth_seal_with(signer_from_seed(2), signers([1, 3]));
assert_eq!(spec.seal(), &SealSource::StealthInput(signer_from_seed(2)));
assert_eq!(spec.authorizers().collect::<Vec<_>>(), vec![
&signer_from_seed(1),
&signer_from_seed(3)
]);
}
#[test]
fn no_signers_seals_ephemerally() {
let spec = SignatureRequirements::stealth_seal(IndexSet::new());
assert_eq!(spec.seal(), &SealSource::Ephemeral);
assert_eq!(spec.authorizers().len(), 0);
}
}
mod merging_two_statements {
use super::*;
#[test]
fn a_displaced_stealth_seal_becomes_an_authorizer() {
let fee = SignatureRequirements::stealth_seal(signers([1]));
let transfer = SignatureRequirements::stealth_seal(signers([2]));
let merged = fee.merge(transfer);
assert_eq!(merged.seal(), &SealSource::StealthInput(signer_from_seed(1)));
assert_eq!(merged.authorizers().cloned().collect::<Vec<_>>(), vec![
signer_from_seed(2)
]);
}
#[test]
fn authorizers_from_both_statements_are_kept() {
let fee = SignatureRequirements::stealth_seal(signers([1, 2]));
let transfer = SignatureRequirements::stealth_seal(signers([3, 4]));
let merged = fee.merge(transfer);
assert_eq!(merged.seal(), &SealSource::StealthInput(signer_from_seed(1)));
assert_eq!(merged.authorizers().cloned().collect::<Vec<_>>(), vec![
signer_from_seed(2),
signer_from_seed(3),
signer_from_seed(4),
]);
}
#[test]
fn an_account_key_seal_outranks_a_stealth_one() {
let fee = SignatureRequirements::stealth_seal(signers([1]));
let transfer = SignatureRequirements::account_key_seal_with(signers([2]));
let merged = fee.clone().merge(transfer.clone());
assert_eq!(merged.seal(), &SealSource::AccountKey);
assert_eq!(merged.authorizers().cloned().collect::<Vec<_>>(), vec![
signer_from_seed(1),
signer_from_seed(2),
]);
assert_eq!(transfer.merge(fee).seal(), &SealSource::AccountKey);
}
#[test]
fn an_ephemeral_seal_yields() {
let ephemeral = SignatureRequirements::stealth_seal(IndexSet::new());
let stealth = SignatureRequirements::stealth_seal(signers([1]));
assert_eq!(
ephemeral.clone().merge(stealth.clone()).seal(),
&SealSource::StealthInput(signer_from_seed(1))
);
assert_eq!(
stealth.merge(ephemeral.clone()).seal(),
&SealSource::StealthInput(signer_from_seed(1))
);
assert_eq!(ephemeral.clone().merge(ephemeral).seal(), &SealSource::Ephemeral);
}
#[test]
fn the_sealing_signer_is_never_also_an_authorizer() {
let fee = SignatureRequirements::stealth_seal(signers([1]));
let transfer = SignatureRequirements::account_key_seal_with(signers([1]));
let merged = fee.clone().merge(transfer);
assert_eq!(merged.authorizers().cloned().collect::<Vec<_>>(), vec![
signer_from_seed(1)
]);
let merged = fee.merge(SignatureRequirements::stealth_seal(signers([1])));
assert_eq!(merged.seal(), &SealSource::StealthInput(signer_from_seed(1)));
assert_eq!(merged.authorizers().len(), 0);
}
}
}