use alloc::string::String;
use alloc::vec::Vec;
use core::cmp::max;
use core::fmt::Debug;
use bytecheck::CheckBytes;
use dusk_bytes::{DeserializableSlice, Error as BytesError};
use poseidon_merkle::Opening;
use rand::{CryptoRng, RngCore};
use rkyv::{Archive, Deserialize, Serialize};
#[cfg(feature = "serde")]
use serde_with::hex::Hex;
#[cfg(feature = "serde")]
use serde_with::{As, DisplayFromStr, Same};
use self::data::{
BlobData, BlobSidecar, ContractCall, ContractDeploy, TransactionData,
};
use self::moonlight::Transaction as MoonlightTransaction;
use self::phoenix::{
NOTES_TREE_DEPTH, Note, Prove, PublicKey as PhoenixPublicKey,
SecretKey as PhoenixSecretKey, Sender, StealthAddress,
Transaction as PhoenixTransaction,
};
use self::withdraw::{Withdraw, WithdrawReceiver};
use crate::abi::ContractId;
use crate::error::TxPreconditionError;
use crate::signatures::bls::{
PublicKey as AccountPublicKey, SecretKey as AccountSecretKey,
};
use crate::{BlsScalar, Error};
pub mod data;
pub mod moonlight;
pub mod phoenix;
pub mod withdraw;
pub const TRANSFER_CONTRACT: ContractId = crate::reserved(0x1);
pub const PANIC_NONCE_NOT_READY: &str = "Nonce not ready to be used yet";
pub const MOONLIGHT_TOPIC: &str = "moonlight";
pub const PHOENIX_TOPIC: &str = "phoenix";
pub const CONTRACT_TO_CONTRACT_TOPIC: &str = "contract_to_contract";
pub const CONTRACT_TO_ACCOUNT_TOPIC: &str = "contract_to_account";
pub const WITHDRAW_TOPIC: &str = "withdraw";
pub const DEPOSIT_TOPIC: &str = "deposit";
pub const CONVERT_TOPIC: &str = "convert";
pub const MINT_TOPIC: &str = "mint";
pub const MINT_CONTRACT_TOPIC: &str = "mint_c";
const BOREAS_FORMAT_VERSION: u8 = 2;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[repr(u8)]
enum TransactionTag {
LegacyPhoenix = 0,
LegacyMoonlight = 1,
VersionedPhoenix = 2,
VersionedMoonlight = 3,
}
impl TryFrom<u8> for TransactionTag {
type Error = BytesError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::LegacyPhoenix),
1 => Ok(Self::LegacyMoonlight),
2 => Ok(Self::VersionedPhoenix),
3 => Ok(Self::VersionedMoonlight),
_ => Err(BytesError::InvalidData),
}
}
}
impl TransactionTag {
fn from_transaction(
transaction: &Transaction,
format: TransactionFormat,
) -> Self {
match (transaction, format) {
(Transaction::Phoenix(_), TransactionFormat::Boreas) => {
Self::VersionedPhoenix
}
(Transaction::Moonlight(_), TransactionFormat::Boreas) => {
Self::VersionedMoonlight
}
(Transaction::Phoenix(_), _) => Self::LegacyPhoenix,
(Transaction::Moonlight(_), _) => Self::LegacyMoonlight,
}
}
fn is_versioned(self) -> bool {
matches!(self, Self::VersionedPhoenix | Self::VersionedMoonlight)
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum TransactionFormat {
PreAegis,
Aegis,
Boreas,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct DecodedTransaction {
pub transaction: Transaction,
pub format: TransactionFormat,
}
#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[allow(clippy::large_enum_variant)]
pub enum Transaction {
Phoenix(PhoenixTransaction),
Moonlight(MoonlightTransaction),
}
impl Transaction {
fn decode_tagged(
tag: TransactionTag,
buf: &[u8],
phoenix_tag: TransactionTag,
moonlight_tag: TransactionTag,
parse_phoenix: fn(&[u8]) -> Result<PhoenixTransaction, BytesError>,
) -> Result<Transaction, BytesError> {
match tag {
tag if tag == phoenix_tag => Ok(Self::Phoenix(parse_phoenix(buf)?)),
tag if tag == moonlight_tag => {
Ok(Self::Moonlight(MoonlightTransaction::from_slice(buf)?))
}
_ => Err(BytesError::InvalidData),
}
}
fn decode_legacy(
format: TransactionFormat,
buf: &[u8],
parse_phoenix: fn(&[u8]) -> Result<PhoenixTransaction, BytesError>,
) -> Result<DecodedTransaction, BytesError> {
let mut buf = buf;
let family_tag = TransactionTag::try_from(u8::from_reader(&mut buf)?)?;
let transaction = Self::decode_tagged(
family_tag,
buf,
TransactionTag::LegacyPhoenix,
TransactionTag::LegacyMoonlight,
parse_phoenix,
)?;
Ok(DecodedTransaction {
transaction,
format,
})
}
fn decode_boreas(buf: &[u8]) -> Result<DecodedTransaction, BytesError> {
let mut buf = buf;
let family_tag = TransactionTag::try_from(u8::from_reader(&mut buf)?)?;
let format_version = u8::from_reader(&mut buf)?;
if format_version != BOREAS_FORMAT_VERSION {
return Err(BytesError::InvalidData);
}
let transaction = Self::decode_tagged(
family_tag,
buf,
TransactionTag::VersionedPhoenix,
TransactionTag::VersionedMoonlight,
PhoenixTransaction::from_slice,
)?;
Ok(DecodedTransaction {
transaction,
format: TransactionFormat::Boreas,
})
}
pub fn decode_with_format(
format: TransactionFormat,
buf: &[u8],
) -> Result<DecodedTransaction, BytesError> {
match format {
TransactionFormat::PreAegis => Self::decode_legacy(
format,
buf,
PhoenixTransaction::from_slice_ledger_compat,
),
TransactionFormat::Aegis => {
Self::decode_legacy(format, buf, PhoenixTransaction::from_slice)
}
TransactionFormat::Boreas => Self::decode_boreas(buf),
}
}
pub fn decode_for_ingress(
format: TransactionFormat,
buf: &[u8],
) -> Result<DecodedTransaction, BytesError> {
Self::decode_with_format(format, buf)
}
pub fn decode_any(buf: &[u8]) -> Result<DecodedTransaction, BytesError> {
let tag = TransactionTag::try_from(
buf.first().copied().ok_or(BytesError::InvalidData)?,
)?;
if tag.is_versioned() {
Self::decode_boreas(buf)
} else {
Self::decode_legacy(
TransactionFormat::Aegis,
buf,
PhoenixTransaction::from_slice,
)
.or_else(|_| {
Self::decode_legacy(
TransactionFormat::PreAegis,
buf,
PhoenixTransaction::from_slice_ledger_compat,
)
})
}
}
#[allow(clippy::too_many_arguments)]
pub fn phoenix<R: RngCore + CryptoRng, P: Prove>(
rng: &mut R,
sender_sk: &PhoenixSecretKey,
refund_pk: &PhoenixPublicKey,
receiver_pk: &PhoenixPublicKey,
inputs: Vec<(Note, Opening<(), NOTES_TREE_DEPTH>)>,
root: BlsScalar,
transfer_value: u64,
obfuscated_transaction: bool,
deposit: u64,
gas_limit: u64,
gas_price: u64,
chain_id: u8,
data: Option<impl Into<TransactionData>>,
prover: &P,
) -> Result<Self, Error> {
Ok(Self::Phoenix(PhoenixTransaction::new::<R, P>(
rng,
sender_sk,
refund_pk,
receiver_pk,
inputs,
root,
transfer_value,
obfuscated_transaction,
deposit,
gas_limit,
gas_price,
chain_id,
data,
prover,
)?))
}
#[allow(clippy::too_many_arguments)]
pub fn moonlight(
sender_sk: &AccountSecretKey,
receiver: Option<AccountPublicKey>,
value: u64,
deposit: u64,
gas_limit: u64,
gas_price: u64,
nonce: u64,
chain_id: u8,
data: Option<impl Into<TransactionData>>,
) -> Result<Self, Error> {
Ok(Self::Moonlight(MoonlightTransaction::new(
sender_sk, receiver, value, deposit, gas_limit, gas_price, nonce,
chain_id, data,
)?))
}
#[must_use]
pub fn moonlight_sender(&self) -> Option<&AccountPublicKey> {
match self {
Self::Phoenix(_) => None,
Self::Moonlight(tx) => Some(tx.sender()),
}
}
#[must_use]
pub fn moonlight_receiver(&self) -> Option<&AccountPublicKey> {
match self {
Self::Phoenix(_) => None,
Self::Moonlight(tx) => tx.receiver(),
}
}
#[must_use]
pub fn value(&self) -> Option<u64> {
match self {
Self::Phoenix(_) => None,
Self::Moonlight(tx) => Some(tx.value()),
}
}
#[must_use]
pub fn nullifiers(&self) -> &[BlsScalar] {
match self {
Self::Phoenix(tx) => tx.nullifiers(),
Self::Moonlight(_) => &[],
}
}
#[must_use]
pub fn root(&self) -> Option<&BlsScalar> {
match self {
Self::Phoenix(tx) => Some(tx.root()),
Self::Moonlight(_) => None,
}
}
#[must_use]
pub fn outputs(&self) -> &[Note] {
match self {
Self::Phoenix(tx) => &tx.outputs()[..],
Self::Moonlight(_) => &[],
}
}
#[must_use]
pub fn phoenix_sender(&self) -> Option<&Sender> {
match self {
Self::Phoenix(tx) => Some(tx.sender()),
Self::Moonlight(_) => None,
}
}
#[must_use]
pub fn deposit(&self) -> u64 {
match self {
Self::Phoenix(tx) => tx.deposit(),
Self::Moonlight(tx) => tx.deposit(),
}
}
#[must_use]
pub fn gas_limit(&self) -> u64 {
match self {
Self::Phoenix(tx) => tx.gas_limit(),
Self::Moonlight(tx) => tx.gas_limit(),
}
}
#[must_use]
pub fn gas_price(&self) -> u64 {
match self {
Self::Phoenix(tx) => tx.gas_price(),
Self::Moonlight(tx) => tx.gas_price(),
}
}
#[must_use]
pub fn refund_address(&self) -> RefundAddress<'_> {
match self {
Self::Phoenix(tx) => RefundAddress::Phoenix(tx.stealth_address()),
Self::Moonlight(tx) => {
RefundAddress::Moonlight(tx.refund_address())
}
}
}
#[must_use]
pub fn call(&self) -> Option<&ContractCall> {
match self {
Self::Phoenix(tx) => tx.call(),
Self::Moonlight(tx) => tx.call(),
}
}
#[must_use]
pub fn deploy(&self) -> Option<&ContractDeploy> {
match self {
Self::Phoenix(tx) => tx.deploy(),
Self::Moonlight(tx) => tx.deploy(),
}
}
#[must_use]
pub fn memo(&self) -> Option<&[u8]> {
match self {
Self::Phoenix(tx) => tx.memo(),
Self::Moonlight(tx) => tx.memo(),
}
}
#[must_use]
pub fn blob(&self) -> Option<&Vec<BlobData>> {
match self {
Self::Phoenix(tx) => tx.blob(),
Self::Moonlight(tx) => tx.blob(),
}
}
#[must_use]
pub fn blob_mut(&mut self) -> Option<&mut Vec<BlobData>> {
match self {
Self::Phoenix(tx) => tx.blob_mut(),
Self::Moonlight(tx) => tx.blob_mut(),
}
}
#[must_use]
pub fn strip_blobs(&mut self) -> Option<Vec<([u8; 32], BlobSidecar)>> {
let blob = match self {
Self::Phoenix(tx) => tx.blob_mut(),
Self::Moonlight(tx) => tx.blob_mut(),
}?;
let ret = blob
.iter_mut()
.filter_map(|b| b.take_sidecar().map(|d| (b.hash, d)))
.collect::<Vec<_>>();
Some(ret)
}
#[must_use]
pub fn blob_to_memo(&self) -> Option<Self> {
Some(match self {
Transaction::Phoenix(tx) => {
Transaction::Phoenix(tx.blob_to_memo()?)
}
Transaction::Moonlight(tx) => {
Transaction::Moonlight(tx.blob_to_memo()?)
}
})
}
#[must_use]
pub fn strip_off_bytecode(&self) -> Option<Self> {
Some(match self {
Transaction::Phoenix(tx) => {
Transaction::Phoenix(tx.strip_off_bytecode()?)
}
Transaction::Moonlight(tx) => {
Transaction::Moonlight(tx.strip_off_bytecode()?)
}
})
}
#[must_use]
pub fn encode_for_format(&self, format: TransactionFormat) -> Vec<u8> {
let mut bytes = Vec::new();
let tag = TransactionTag::from_transaction(self, format);
bytes.push(tag as u8);
if tag.is_versioned() {
bytes.push(BOREAS_FORMAT_VERSION);
}
match self {
Self::Phoenix(tx) => bytes.extend(tx.to_var_bytes()),
Self::Moonlight(tx) => bytes.extend(tx.to_var_bytes()),
}
bytes
}
#[must_use]
pub fn to_network_bytes(&self) -> Vec<u8> {
self.encode_for_format(TransactionFormat::Aegis)
}
#[must_use]
pub fn to_var_bytes(&self) -> Vec<u8> {
self.to_network_bytes()
}
pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
Self::decode_any(buf).map(|decoded| decoded.transaction)
}
#[must_use]
pub fn to_hash_input_bytes(&self) -> Vec<u8> {
match self {
Self::Phoenix(tx) => tx.to_hash_input_bytes(),
Self::Moonlight(tx) => tx.to_hash_input_bytes(),
}
}
#[must_use]
pub fn hash(&self) -> BlsScalar {
match self {
Self::Phoenix(tx) => tx.hash(),
Self::Moonlight(tx) => tx.hash(),
}
}
pub fn deploy_charge(
&self,
gas_per_deploy_byte: u64,
min_deploy_points: u64,
) -> Result<u64, TxPreconditionError> {
if let Some(deploy) = self.deploy() {
let bytecode_len = deploy.bytecode.bytes.len() as u64;
let deploy_charge =
bytecode_len
.checked_mul(gas_per_deploy_byte)
.ok_or(TxPreconditionError::DeployChargeOverflow)?;
Ok(max(deploy_charge, min_deploy_points))
} else {
Ok(0)
}
}
pub fn blob_charge(
&self,
gas_per_blob: u64,
) -> Result<Option<u64>, TxPreconditionError> {
self.blob()
.map(|blobs| {
(blobs.len() as u64)
.checked_mul(gas_per_blob)
.ok_or(TxPreconditionError::BlobChargeOverflow)
})
.transpose()
}
pub fn phoenix_fee_check(&self) -> Result<(), TxPreconditionError> {
if let Transaction::Phoenix(tx) = self {
let max_fee = tx
.fee()
.gas_limit
.checked_mul(tx.fee().gas_price)
.ok_or(TxPreconditionError::PhoenixFeeOverflow)?;
if max_fee != tx.max_fee() {
return Err(TxPreconditionError::PhoenixFeeTampered);
}
}
Ok(())
}
pub fn phoenix_refund_check(&self) -> Result<(), TxPreconditionError> {
if let Transaction::Phoenix(tx) = self
&& tx.fee().stealth_address != *tx.outputs()[1].stealth_address()
{
return Err(TxPreconditionError::PhoenixFeeRefundMismatch);
}
Ok(())
}
pub fn deploy_check(
&self,
gas_per_deploy_byte: u64,
min_deploy_gas_price: u64,
min_deploy_points: u64,
) -> Result<(), TxPreconditionError> {
if self.deploy().is_some() {
let deploy_charge =
self.deploy_charge(gas_per_deploy_byte, min_deploy_points)?;
if self.gas_price() < min_deploy_gas_price {
return Err(TxPreconditionError::DeployLowPrice(
min_deploy_gas_price,
));
}
if self.gas_limit() < deploy_charge {
return Err(TxPreconditionError::DeployLowLimit(deploy_charge));
}
}
Ok(())
}
pub fn blob_check(
&self,
gas_per_blob: u64,
) -> Result<Option<u64>, TxPreconditionError> {
if let Some(blobs) = self.blob() {
match blobs.len() {
0 => Err(TxPreconditionError::BlobEmpty),
n if n > 6 => Err(TxPreconditionError::BlobTooMany(n)),
_ => Ok(()),
}?;
} else {
return Ok(None);
}
let min_charge = self.blob_charge(gas_per_blob)?;
if let Some(min_charge) = min_charge
&& self.gas_limit() < min_charge
{
return Err(TxPreconditionError::BlobLowLimit(min_charge));
}
Ok(min_charge)
}
}
impl From<PhoenixTransaction> for Transaction {
fn from(tx: PhoenixTransaction) -> Self {
Self::Phoenix(tx)
}
}
impl From<MoonlightTransaction> for Transaction {
fn from(tx: MoonlightTransaction) -> Self {
Self::Moonlight(tx)
}
}
pub enum RefundAddress<'a> {
Phoenix(&'a StealthAddress),
Moonlight(&'a AccountPublicKey),
}
#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContractToContract {
pub contract: ContractId,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
pub fn_name: String,
#[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ReceiveFromContract {
pub contract: ContractId,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
#[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Archive, PartialEq, Eq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContractToAccount {
pub account: AccountPublicKey,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WithdrawEvent {
pub sender: ContractId,
pub receiver: WithdrawReceiver,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
}
impl From<Withdraw> for WithdrawEvent {
fn from(w: Withdraw) -> Self {
Self {
sender: w.contract,
receiver: w.receiver,
value: w.value,
}
}
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConvertEvent {
pub sender: Option<AccountPublicKey>,
pub receiver: WithdrawReceiver,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
}
impl ConvertEvent {
#[must_use]
pub fn from_withdraw_and_sender(
sender: Option<AccountPublicKey>,
withdraw: &Withdraw,
) -> Self {
Self {
sender,
receiver: withdraw.receiver,
value: withdraw.value,
}
}
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DepositEvent {
pub sender: Option<AccountPublicKey>,
pub receiver: ContractId,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContractToContractEvent {
pub sender: ContractId,
pub receiver: ContractId,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContractToAccountEvent {
pub sender: ContractId,
pub receiver: AccountPublicKey,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PhoenixTransactionEvent {
pub nullifiers: Vec<BlsScalar>,
pub notes: Vec<Note>,
#[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
pub memo: Vec<u8>,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub gas_spent: u64,
pub refund_note: Option<Note>,
}
#[derive(Debug, Clone, Archive, PartialEq, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MoonlightTransactionEvent {
pub sender: AccountPublicKey,
pub receiver: Option<AccountPublicKey>,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub value: u64,
#[cfg_attr(feature = "serde", serde(with = "As::<Hex>"))]
pub memo: Vec<u8>,
#[cfg_attr(feature = "serde", serde(with = "As::<DisplayFromStr>"))]
pub gas_spent: u64,
#[cfg_attr(
feature = "serde",
serde(with = "As::<Option<(Same, DisplayFromStr)>>")
)]
pub refund_info: Option<(AccountPublicKey, u64)>,
}