use alloc::vec::Vec;
use core::cmp;
use core::fmt::Debug;
use bytecheck::CheckBytes;
use dusk_bytes::{DeserializableSlice, Error as BytesError, Serializable};
use dusk_poseidon::{Domain, Hash};
use ff::Field;
use rand::{CryptoRng, RngCore};
use rkyv::{Archive, Deserialize, Serialize};
use crate::signatures::schnorr::{
SecretKey as SchnorrSecretKey, Signature as SchnorrSignature,
};
use crate::transfer::data::{
BlobData, ContractBytecode, ContractCall, ContractDeploy, TransactionData,
MAX_MEMO_SIZE,
};
use crate::{BlsScalar, Error, JubJubAffine, JubJubScalar};
pub use phoenix_circuits::{InputNoteInfo, OutputNoteInfo, TxCircuit};
pub use phoenix_core::{
value_commitment, Error as CoreError, Note, PublicKey, SecretKey, Sender,
StealthAddress, TxSkeleton, ViewKey, NOTE_VAL_ENC_SIZE, OUTPUT_NOTES,
};
pub const NOTES_TREE_DEPTH: usize = 17;
pub use poseidon_merkle::ARITY as NOTES_TREE_ARITY;
pub type NotesTree = poseidon_merkle::Tree<(), NOTES_TREE_DEPTH>;
pub type NoteOpening = poseidon_merkle::Opening<(), NOTES_TREE_DEPTH>;
pub type NoteTreeItem = poseidon_merkle::Item<()>;
#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
pub struct NoteLeaf {
pub block_height: u64,
pub note: Note,
}
impl AsRef<Note> for NoteLeaf {
fn as_ref(&self) -> &Note {
&self.note
}
}
impl cmp::Ord for NoteLeaf {
fn cmp(&self, other: &Self) -> cmp::Ordering {
self.note.pos().cmp(other.note.pos())
}
}
impl cmp::PartialOrd for NoteLeaf {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
pub const TRANSCRIPT_LABEL: &[u8] = b"dusk-network";
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
pub struct Transaction {
payload: Payload,
proof: Vec<u8>,
}
impl PartialEq for Transaction {
fn eq(&self, other: &Self) -> bool {
self.hash() == other.hash()
}
}
impl Eq for Transaction {}
impl Transaction {
#[allow(clippy::too_many_lines)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::similar_names)]
pub fn new<R: RngCore + CryptoRng, P: Prove>(
rng: &mut R,
sender_sk: &SecretKey,
refund_pk: &PublicKey,
receiver_pk: &PublicKey,
inputs: Vec<(Note, NoteOpening)>,
root: BlsScalar,
transfer_value: u64,
obfuscate_transfer_note: bool,
deposit: u64,
gas_limit: u64,
gas_price: u64,
chain_id: u8,
data: Option<impl Into<TransactionData>>,
prover: &P,
) -> Result<Self, Error> {
let data = data.map(Into::into);
if let Some(TransactionData::Memo(memo)) = data.as_ref() {
if memo.len() > MAX_MEMO_SIZE {
return Err(Error::MemoTooLarge(memo.len()));
}
}
let sender_pk = PublicKey::from(sender_sk);
let sender_vk = ViewKey::from(sender_sk);
let input_len = inputs.len();
let mut input_values = Vec::with_capacity(input_len);
let mut input_value_blinders = Vec::with_capacity(input_len);
let mut input_nullifiers = Vec::with_capacity(input_len);
for (note, _opening) in &inputs {
let note_nullifier = note.gen_nullifier(sender_sk);
for nullifier in &input_nullifiers {
if note_nullifier == *nullifier {
return Err(Error::Replay);
}
}
input_nullifiers.push(note_nullifier);
input_values.push(note.value(Some(&sender_vk))?);
input_value_blinders.push(note.value_blinder(Some(&sender_vk))?);
}
let input_value: u64 = input_values.iter().sum();
let fee = Fee::new(rng, refund_pk, gas_limit, gas_price);
let max_fee = fee.max_fee();
if input_value < transfer_value + max_fee + deposit {
return Err(Error::InsufficientBalance);
}
let transfer_value_blinder = if obfuscate_transfer_note {
JubJubScalar::random(&mut *rng)
} else {
JubJubScalar::zero()
};
let transfer_sender_blinder = [
JubJubScalar::random(&mut *rng),
JubJubScalar::random(&mut *rng),
];
let change_sender_blinder = [
JubJubScalar::random(&mut *rng),
JubJubScalar::random(&mut *rng),
];
let transfer_note = if obfuscate_transfer_note {
Note::obfuscated(
rng,
&sender_pk,
receiver_pk,
transfer_value,
transfer_value_blinder,
transfer_sender_blinder,
)
} else {
Note::transparent(
rng,
&sender_pk,
receiver_pk,
transfer_value,
transfer_sender_blinder,
)
};
let change_value = input_value - transfer_value - max_fee - deposit;
let change_value_blinder = JubJubScalar::random(&mut *rng);
let change_note = Note::obfuscated(
rng,
&sender_pk,
refund_pk,
change_value,
change_value_blinder,
change_sender_blinder,
);
let outputs = [transfer_note.clone(), change_note.clone()];
let tx_skeleton = TxSkeleton {
root,
nullifiers: input_nullifiers.clone(),
outputs,
max_fee,
deposit,
};
let payload = Payload {
chain_id,
tx_skeleton,
fee,
data,
};
let payload_hash = payload.hash();
let mut input_notes_info = Vec::with_capacity(input_len);
inputs
.into_iter()
.zip(input_nullifiers)
.zip(input_values)
.zip(input_value_blinders)
.for_each(
|(
(((note, merkle_opening), nullifier), value),
value_blinder,
)| {
let note_sk = sender_sk.gen_note_sk(note.stealth_address());
let note_pk_p = JubJubAffine::from(
crate::GENERATOR_NUMS_EXTENDED * note_sk.as_ref(),
);
let signature = note_sk.sign_double(rng, payload_hash);
input_notes_info.push(InputNoteInfo {
merkle_opening,
note,
note_pk_p,
value,
value_blinder,
nullifier,
signature,
});
},
);
let transfer_value_commitment =
value_commitment(transfer_value, transfer_value_blinder);
let transfer_note_sender_enc = match transfer_note.sender() {
Sender::Encryption(enc) => enc,
Sender::ContractInfo(_) => unreachable!("The sender is encrypted"),
};
let change_value_commitment =
value_commitment(change_value, change_value_blinder);
let change_note_sender_enc = match change_note.sender() {
Sender::Encryption(enc) => enc,
Sender::ContractInfo(_) => unreachable!("The sender is encrypted"),
};
let output_notes_info = [
OutputNoteInfo {
value: transfer_value,
value_commitment: transfer_value_commitment,
value_blinder: transfer_value_blinder,
note_pk: JubJubAffine::from(
transfer_note.stealth_address().note_pk().as_ref(),
),
sender_enc: *transfer_note_sender_enc,
sender_blinder: transfer_sender_blinder,
},
OutputNoteInfo {
value: change_value,
value_commitment: change_value_commitment,
value_blinder: change_value_blinder,
note_pk: JubJubAffine::from(
change_note.stealth_address().note_pk().as_ref(),
),
sender_enc: *change_note_sender_enc,
sender_blinder: change_sender_blinder,
},
];
let schnorr_sk_a = SchnorrSecretKey::from(sender_sk.a());
let sig_a = schnorr_sk_a.sign(rng, payload_hash);
let schnorr_sk_b = SchnorrSecretKey::from(sender_sk.b());
let sig_b = schnorr_sk_b.sign(rng, payload_hash);
Ok(Self {
payload,
proof: prover.prove(
&TxCircuitVec {
input_notes_info,
output_notes_info,
payload_hash,
root,
deposit,
max_fee,
sender_pk,
signatures: (sig_a, sig_b),
}
.to_var_bytes(),
)?,
})
}
#[must_use]
pub fn from_payload_and_proof(payload: Payload, proof: Vec<u8>) -> Self {
Self { payload, proof }
}
pub fn set_proof(&mut self, proof: Vec<u8>) {
self.proof = proof;
}
#[must_use]
pub fn proof(&self) -> &[u8] {
&self.proof
}
#[must_use]
pub fn payload_hash(&self) -> BlsScalar {
self.payload.hash()
}
#[must_use]
pub fn nullifiers(&self) -> &[BlsScalar] {
&self.payload.tx_skeleton.nullifiers
}
#[must_use]
pub fn root(&self) -> &BlsScalar {
&self.payload.tx_skeleton.root
}
#[must_use]
pub fn outputs(&self) -> &[Note; OUTPUT_NOTES] {
&self.payload.tx_skeleton.outputs
}
#[must_use]
pub fn fee(&self) -> &Fee {
&self.payload.fee
}
#[must_use]
pub fn stealth_address(&self) -> &StealthAddress {
&self.payload.fee.stealth_address
}
#[must_use]
pub fn sender(&self) -> &Sender {
&self.payload.fee.sender
}
#[must_use]
pub fn gas_limit(&self) -> u64 {
self.payload.fee.gas_limit
}
#[must_use]
pub fn gas_price(&self) -> u64 {
self.payload.fee.gas_price
}
#[must_use]
pub fn chain_id(&self) -> u8 {
self.payload.chain_id
}
#[must_use]
pub fn max_fee(&self) -> u64 {
self.payload.tx_skeleton.max_fee
}
#[must_use]
pub fn deposit(&self) -> u64 {
self.payload.tx_skeleton.deposit
}
#[must_use]
pub fn call(&self) -> Option<&ContractCall> {
#[allow(clippy::match_wildcard_for_single_variants)]
match self.data()? {
TransactionData::Call(ref c) => Some(c),
_ => None,
}
}
#[must_use]
pub fn deploy(&self) -> Option<&ContractDeploy> {
#[allow(clippy::match_wildcard_for_single_variants)]
match self.data()? {
TransactionData::Deploy(ref d) => Some(d),
_ => None,
}
}
#[must_use]
pub fn blob(&self) -> Option<&Vec<BlobData>> {
#[allow(clippy::match_wildcard_for_single_variants)]
match self.data()? {
TransactionData::Blob(ref d) => Some(d),
_ => None,
}
}
#[must_use]
pub fn blob_mut(&mut self) -> Option<&mut Vec<BlobData>> {
#[allow(clippy::match_wildcard_for_single_variants)]
match self.data_mut()? {
TransactionData::Blob(d) => Some(d),
_ => None,
}
}
#[must_use]
pub fn memo(&self) -> Option<&[u8]> {
match self.data()? {
TransactionData::Memo(memo) => Some(memo),
_ => None,
}
}
#[must_use]
pub(crate) fn data(&self) -> Option<&TransactionData> {
self.payload.data.as_ref()
}
#[must_use]
pub(crate) fn data_mut(&mut self) -> Option<&mut TransactionData> {
self.payload.data.as_mut()
}
#[must_use]
pub fn strip_off_bytecode(&self) -> Option<Self> {
let deploy = self.deploy()?;
let stripped_deploy = TransactionData::Deploy(ContractDeploy {
owner: deploy.owner.clone(),
init_args: deploy.init_args.clone(),
bytecode: ContractBytecode {
hash: deploy.bytecode.hash,
bytes: Vec::new(),
},
nonce: deploy.nonce,
});
let mut stripped_transaction = self.clone();
stripped_transaction.payload.data = Some(stripped_deploy);
Some(stripped_transaction)
}
#[must_use]
pub fn blob_to_memo(&self) -> Option<Self> {
let data = self.data()?;
if let TransactionData::Blob(_) = data {
let hash = data.signature_message();
let memo = TransactionData::Memo(hash);
let mut converted_tx = self.clone();
converted_tx.payload.data = Some(memo);
Some(converted_tx)
} else {
None
}
}
#[must_use]
pub fn to_var_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let payload_bytes = self.payload.to_var_bytes();
bytes.extend((payload_bytes.len() as u64).to_bytes());
bytes.extend(payload_bytes);
bytes.extend((self.proof.len() as u64).to_bytes());
bytes.extend(&self.proof);
bytes
}
pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
let mut buf = buf;
let payload_len = usize::try_from(u64::from_reader(&mut buf)?)
.map_err(|_| BytesError::InvalidData)?;
if buf.len() < payload_len {
return Err(BytesError::InvalidData);
}
let (payload_buf, new_buf) = buf.split_at(payload_len);
let payload = Payload::from_slice(payload_buf)?;
buf = new_buf;
let proof_len = usize::try_from(u64::from_reader(&mut buf)?)
.map_err(|_| BytesError::InvalidData)?;
let proof = buf[..proof_len].into();
Ok(Self { payload, proof })
}
#[must_use]
pub fn to_hash_input_bytes(&self) -> Vec<u8> {
let mut bytes = self.payload.to_hash_input_bytes();
bytes.extend(&self.proof);
bytes
}
#[must_use]
pub fn hash(&self) -> BlsScalar {
BlsScalar::hash_to_scalar(&self.to_hash_input_bytes())
}
#[must_use]
pub fn public_inputs(&self) -> Vec<BlsScalar> {
let tx_skeleton = &self.payload.tx_skeleton;
let input_len = tx_skeleton.nullifiers.len();
let output_len = tx_skeleton.outputs.len();
let size =
1 + 1
+ input_len
+ 2 * output_len
+ 1 + 1
+ 2 * output_len
+ 2 * 4 * output_len;
let mut pis = Vec::<BlsScalar>::with_capacity(size);
pis.push(self.payload.hash());
pis.push(tx_skeleton.root);
pis.extend(tx_skeleton.nullifiers().iter());
tx_skeleton.outputs().iter().for_each(|note| {
let value_commitment = note.value_commitment();
pis.push(value_commitment.get_u());
pis.push(value_commitment.get_v());
});
pis.push(tx_skeleton.max_fee().into());
pis.push(tx_skeleton.deposit().into());
tx_skeleton.outputs().iter().for_each(|note| {
let note_pk =
JubJubAffine::from(note.stealth_address().note_pk().as_ref());
pis.push(note_pk.get_u());
pis.push(note_pk.get_v());
});
tx_skeleton.outputs().iter().for_each(|note| {
match note.sender() {
Sender::Encryption(sender_enc) => {
pis.push(sender_enc[0].0.get_u());
pis.push(sender_enc[0].0.get_v());
pis.push(sender_enc[0].1.get_u());
pis.push(sender_enc[0].1.get_v());
pis.push(sender_enc[1].0.get_u());
pis.push(sender_enc[1].0.get_v());
pis.push(sender_enc[1].1.get_u());
pis.push(sender_enc[1].1.get_v());
}
Sender::ContractInfo(_) => {
panic!("All output-notes must provide a sender-encryption")
}
};
});
pis
}
}
#[derive(Debug, Clone, Archive, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
pub struct Payload {
pub chain_id: u8,
pub tx_skeleton: TxSkeleton,
pub fee: Fee,
pub data: Option<TransactionData>,
}
impl PartialEq for Payload {
fn eq(&self, other: &Self) -> bool {
self.hash() == other.hash()
}
}
impl Eq for Payload {}
impl Payload {
#[must_use]
pub fn to_var_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::from([self.chain_id]);
let skeleton_bytes = self.tx_skeleton.to_var_bytes();
bytes.extend((skeleton_bytes.len() as u64).to_bytes());
bytes.extend(skeleton_bytes);
bytes.extend(self.fee.to_bytes());
bytes.extend(TransactionData::option_to_var_bytes(self.data.as_ref()));
bytes
}
pub fn from_slice(buf: &[u8]) -> Result<Self, BytesError> {
let mut buf = buf;
let chain_id = u8::from_reader(&mut buf)?;
#[allow(clippy::cast_possible_truncation)]
let skeleton_len = usize::try_from(u64::from_reader(&mut buf)?)
.map_err(|_| BytesError::InvalidData)?;
let tx_skeleton = TxSkeleton::from_slice(buf)?;
buf = &buf[skeleton_len..];
let fee = Fee::from_reader(&mut buf)?;
let data = TransactionData::from_slice(buf)?;
Ok(Self {
chain_id,
tx_skeleton,
fee,
data,
})
}
#[must_use]
pub fn to_hash_input_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::from([self.chain_id]);
bytes.extend(self.tx_skeleton.to_hash_input_bytes());
if let Some(data) = &self.data {
bytes.extend(data.signature_message());
}
bytes
}
#[must_use]
pub fn hash(&self) -> BlsScalar {
BlsScalar::hash_to_scalar(&self.to_hash_input_bytes())
}
}
#[derive(Debug, Clone, Copy, Archive, Serialize, Deserialize)]
#[archive_attr(derive(CheckBytes))]
pub struct Fee {
pub gas_limit: u64,
pub gas_price: u64,
pub stealth_address: StealthAddress,
pub sender: Sender,
}
impl PartialEq for Fee {
fn eq(&self, other: &Self) -> bool {
self.sender == other.sender && self.hash() == other.hash()
}
}
impl Eq for Fee {}
impl Fee {
#[must_use]
pub fn new<R: RngCore + CryptoRng>(
rng: &mut R,
refund_pk: &PublicKey,
gas_limit: u64,
gas_price: u64,
) -> Self {
let r = JubJubScalar::random(&mut *rng);
let sender_blinder = [
JubJubScalar::random(&mut *rng),
JubJubScalar::random(&mut *rng),
];
Self::deterministic(
&r,
refund_pk,
gas_limit,
gas_price,
&sender_blinder,
)
}
#[must_use]
pub fn deterministic(
r: &JubJubScalar,
refund_pk: &PublicKey,
gas_limit: u64,
gas_price: u64,
sender_blinder: &[JubJubScalar; 2],
) -> Self {
let refund_address = refund_pk.gen_stealth_address(r);
let sender = Sender::encrypt(
refund_address.note_pk(),
refund_pk,
sender_blinder,
);
Fee {
gas_limit,
gas_price,
stealth_address: refund_address,
sender,
}
}
#[must_use]
pub fn max_fee(&self) -> u64 {
self.gas_limit * self.gas_price
}
#[must_use]
pub fn hash(&self) -> BlsScalar {
let npk = self.stealth_address.note_pk().as_ref().to_hash_inputs();
let hash_inputs = [
BlsScalar::from(self.gas_limit),
BlsScalar::from(self.gas_price),
npk[0],
npk[1],
];
Hash::digest(Domain::Other, &hash_inputs)[0]
}
#[must_use]
pub fn gen_remainder_note(
&self,
gas_consumed: u64,
deposit: Option<u64>,
) -> Note {
let gas_consumed = cmp::min(gas_consumed, self.gas_limit);
let gas_changes = (self.gas_limit - gas_consumed) * self.gas_price;
Note::transparent_stealth(
self.stealth_address,
gas_changes + deposit.unwrap_or_default(),
self.sender,
)
}
}
const SIZE: usize = 2 * u64::SIZE + StealthAddress::SIZE + Sender::SIZE;
impl Serializable<SIZE> for Fee {
type Error = BytesError;
#[must_use]
fn to_bytes(&self) -> [u8; Self::SIZE] {
let mut buf = [0u8; Self::SIZE];
buf[..u64::SIZE].copy_from_slice(&self.gas_limit.to_bytes());
let mut start = u64::SIZE;
buf[start..start + u64::SIZE]
.copy_from_slice(&self.gas_price.to_bytes());
start += u64::SIZE;
buf[start..start + StealthAddress::SIZE]
.copy_from_slice(&self.stealth_address.to_bytes());
start += StealthAddress::SIZE;
buf[start..start + Sender::SIZE]
.copy_from_slice(&self.sender.to_bytes());
buf
}
fn from_bytes(bytes: &[u8; Self::SIZE]) -> Result<Self, Self::Error> {
let mut reader = &bytes[..];
let gas_limit = u64::from_reader(&mut reader)?;
let gas_price = u64::from_reader(&mut reader)?;
let refund_address = StealthAddress::from_reader(&mut reader)?;
let sender = Sender::from_reader(&mut reader)?;
Ok(Fee {
gas_limit,
gas_price,
stealth_address: refund_address,
sender,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TxCircuitVec {
pub input_notes_info: Vec<InputNoteInfo<NOTES_TREE_DEPTH>>,
pub output_notes_info: [OutputNoteInfo; OUTPUT_NOTES],
pub payload_hash: BlsScalar,
pub root: BlsScalar,
pub deposit: u64,
pub max_fee: u64,
pub sender_pk: PublicKey,
pub signatures: (SchnorrSignature, SchnorrSignature),
}
impl TxCircuitVec {
#[must_use]
pub fn to_var_bytes(&self) -> Vec<u8> {
let input_len = self.input_notes_info.len();
let mut bytes = Vec::with_capacity(Self::size(input_len));
bytes.extend((input_len as u64).to_bytes());
for info in &self.input_notes_info {
bytes.extend(info.to_var_bytes());
}
for info in &self.output_notes_info {
bytes.extend(info.to_bytes());
}
bytes.extend(self.payload_hash.to_bytes());
bytes.extend(self.root.to_bytes());
bytes.extend(self.deposit.to_bytes());
bytes.extend(self.max_fee.to_bytes());
bytes.extend(self.sender_pk.to_bytes());
bytes.extend(self.signatures.0.to_bytes());
bytes.extend(self.signatures.1.to_bytes());
bytes
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, BytesError> {
let input_len = u64::from_slice(bytes)?;
#[allow(clippy::cast_possible_truncation)]
if bytes.len() < Self::size(input_len as usize) {
return Err(BytesError::BadLength {
found: bytes.len(),
expected: Self::size(input_len as usize),
});
}
let bytes = &bytes[u64::SIZE..];
let circuit: TxCircuitVec = match input_len {
1 => TxCircuit::<NOTES_TREE_DEPTH, 1>::from_slice(bytes)?.into(),
2 => TxCircuit::<NOTES_TREE_DEPTH, 2>::from_slice(bytes)?.into(),
3 => TxCircuit::<NOTES_TREE_DEPTH, 3>::from_slice(bytes)?.into(),
4 => TxCircuit::<NOTES_TREE_DEPTH, 4>::from_slice(bytes)?.into(),
_ => return Err(BytesError::InvalidData),
};
Ok(circuit)
}
const fn size(input_len: usize) -> usize {
u64::SIZE
+ input_len * InputNoteInfo::<NOTES_TREE_DEPTH>::SIZE
+ OUTPUT_NOTES * OutputNoteInfo::SIZE
+ 2 * BlsScalar::SIZE
+ 2 * u64::SIZE
+ PublicKey::SIZE
+ 2 * SchnorrSignature::SIZE
}
}
impl<const I: usize> From<TxCircuit<NOTES_TREE_DEPTH, I>> for TxCircuitVec {
fn from(circuit: TxCircuit<NOTES_TREE_DEPTH, I>) -> Self {
TxCircuitVec {
input_notes_info: circuit.input_notes_info.to_vec(),
output_notes_info: circuit.output_notes_info,
payload_hash: circuit.payload_hash,
root: circuit.root,
deposit: circuit.deposit,
max_fee: circuit.max_fee,
sender_pk: circuit.sender_pk,
signatures: circuit.signatures,
}
}
}
pub trait Prove {
fn prove(&self, tx_circuit_vec_bytes: &[u8]) -> Result<Vec<u8>, Error>;
}