mod bytes;
mod serialize;
mod string;
use console::{
account::{Address, ViewKey},
network::prelude::*,
program::{Ciphertext, Future, Plaintext, Record, TransitionLeaf, ValueType},
types::{Field, Group},
};
type Variant = u8;
#[derive(Clone, PartialEq, Eq)]
pub enum Output<N: Network> {
Constant(Field<N>, Option<Plaintext<N>>),
Public(Field<N>, Option<Plaintext<N>>),
Private(Field<N>, Option<Ciphertext<N>>),
Record(Field<N>, Field<N>, Option<Record<N, Ciphertext<N>>>, Option<Field<N>>),
ExternalRecord(Field<N>),
Future(Field<N>, Option<Future<N>>),
DynamicRecord(Field<N>),
RecordWithDynamicID(Field<N>, Field<N>, Option<Record<N, Ciphertext<N>>>, Option<Field<N>>, Field<N>),
ExternalRecordWithDynamicID(Field<N>, Field<N>),
}
impl<N: Network> Output<N> {
pub const fn variant(&self) -> Variant {
match self {
Output::Constant(_, _) => 0,
Output::Public(_, _) => 1,
Output::Private(_, _) => 2,
Output::Record(_, _, _, _) => 3,
Output::ExternalRecord(_) => 4,
Output::Future(_, _) => 5,
Output::DynamicRecord(_) => 6,
Output::RecordWithDynamicID(..) => 7,
Output::ExternalRecordWithDynamicID(..) => 8,
}
}
pub const fn id(&self) -> &Field<N> {
match self {
Output::Constant(id, ..) => id,
Output::Public(id, ..) => id,
Output::Private(id, ..) => id,
Output::Record(commitment, ..) => commitment,
Output::ExternalRecord(id) => id,
Output::Future(id, ..) => id,
Output::DynamicRecord(id) => id,
Output::RecordWithDynamicID(commitment, ..) => commitment,
Output::ExternalRecordWithDynamicID(id, ..) => id,
}
}
pub fn to_transition_leaf(&self, index: u8) -> TransitionLeaf<N> {
match self {
Output::RecordWithDynamicID(..) => TransitionLeaf::new_record_with_dynamic_id(index, *self.id()),
Output::ExternalRecordWithDynamicID(..) => {
TransitionLeaf::new_external_record_with_dynamic_id(index, *self.id())
}
_ => TransitionLeaf::new(index, self.variant(), *self.id()),
}
}
#[allow(clippy::type_complexity)]
pub const fn record(&self) -> Option<(&Field<N>, &Record<N, Ciphertext<N>>)> {
match self {
Output::Record(commitment, _, Some(record), _)
| Output::RecordWithDynamicID(commitment, _, Some(record), _, _) => Some((commitment, record)),
_ => None,
}
}
#[allow(clippy::type_complexity)]
pub fn into_record(self) -> Option<(Field<N>, Record<N, Ciphertext<N>>)> {
match self {
Output::Record(commitment, _, Some(record), _)
| Output::RecordWithDynamicID(commitment, _, Some(record), _, _) => Some((commitment, record)),
_ => None,
}
}
pub const fn commitment(&self) -> Option<&Field<N>> {
match self {
Output::Record(commitment, ..) | Output::RecordWithDynamicID(commitment, ..) => Some(commitment),
_ => None,
}
}
pub fn into_commitment(self) -> Option<Field<N>> {
match self {
Output::Record(commitment, ..) | Output::RecordWithDynamicID(commitment, ..) => Some(commitment),
_ => None,
}
}
pub const fn nonce(&self) -> Option<&Group<N>> {
match self {
Output::Record(_, _, Some(record), _) | Output::RecordWithDynamicID(_, _, Some(record), _, _) => {
Some(record.nonce())
}
_ => None,
}
}
pub fn into_nonce(self) -> Option<Group<N>> {
match self {
Output::Record(_, _, Some(record), _) | Output::RecordWithDynamicID(_, _, Some(record), _, _) => {
Some(record.into_nonce())
}
_ => None,
}
}
pub const fn checksum(&self) -> Option<&Field<N>> {
match self {
Output::Record(_, checksum, ..) | Output::RecordWithDynamicID(_, checksum, ..) => Some(checksum),
_ => None,
}
}
pub fn into_checksum(self) -> Option<Field<N>> {
match self {
Output::Record(_, checksum, ..) | Output::RecordWithDynamicID(_, checksum, ..) => Some(checksum),
_ => None,
}
}
pub const fn sender_ciphertext(&self) -> Option<&Field<N>> {
match self {
Output::Record(_, _, _, Some(sender_ciphertext))
| Output::RecordWithDynamicID(_, _, _, Some(sender_ciphertext), _) => Some(sender_ciphertext),
_ => None,
}
}
pub fn into_sender_ciphertext(self) -> Option<Field<N>> {
match self {
Output::Record(_, _, _, Some(sender_ciphertext))
| Output::RecordWithDynamicID(_, _, _, Some(sender_ciphertext), _) => Some(sender_ciphertext),
_ => None,
}
}
pub const fn future(&self) -> Option<&Future<N>> {
match self {
Output::Future(_, Some(future)) => Some(future),
_ => None,
}
}
}
impl<N: Network> Output<N> {
pub fn decrypt_sender_ciphertext(&self, account_view_key: &ViewKey<N>) -> Result<Option<Address<N>>> {
let (record_ciphertext, sender_ciphertext) = match self {
Output::Record(_, _, Some(record_ciphertext), Some(sender_ciphertext))
| Output::RecordWithDynamicID(_, _, Some(record_ciphertext), Some(sender_ciphertext), _) => {
(record_ciphertext, sender_ciphertext)
}
_ => return Ok(None),
};
let record_view_key = (*record_ciphertext.nonce() * **account_view_key).to_x_coordinate();
let expected_owner = match record_ciphertext.owner().is_public() {
true => record_ciphertext.owner().decrypt_with_randomizer(&[])?,
false => {
let randomizers = N::hash_many_psd8(&[N::encryption_domain(), record_view_key], 1);
ensure!(randomizers.len() == 1, "Expected exactly one randomizer for the record owner");
record_ciphertext.owner().decrypt_with_randomizer(&[randomizers[0]])?
}
};
ensure!(
*expected_owner == account_view_key.to_address(),
"The record does not belong to the given account view key"
);
let Ok(randomizer) = N::hash_psd4(&[N::encryption_domain(), record_view_key, Field::one()]) else {
bail!("Failed to compute the encryption randomizer for the sender ciphertext");
};
let sender_x_coordinate = *sender_ciphertext - randomizer;
match Address::from_field(&sender_x_coordinate) {
Ok(sender_address) => Ok(Some(sender_address)),
Err(error) => bail!("Failed to recover the sender address - {error}"),
}
}
}
impl<N: Network> Output<N> {
pub fn verifier_inputs(&self) -> impl '_ + Iterator<Item = N::Field> {
[**self.id()].into_iter()
.chain([self.checksum().map(|sum| **sum), self.sender_ciphertext().map(|sender| **sender)].into_iter().flatten())
}
pub const fn dynamic_id(&self) -> Option<&Field<N>> {
match self {
Output::RecordWithDynamicID(_, _, _, _, dynamic_id)
| Output::ExternalRecordWithDynamicID(_, dynamic_id) => Some(dynamic_id),
_ => None,
}
}
pub fn to_caller_output(&self) -> Self {
match self {
Self::RecordWithDynamicID(_, _, _, _, dynamic_id) => Self::DynamicRecord(*dynamic_id),
Self::ExternalRecordWithDynamicID(_, dynamic_id) => Self::DynamicRecord(*dynamic_id),
other => other.clone(),
}
}
pub fn verify(&self, function_id: Field<N>, tcm: &Field<N>, index: usize) -> bool {
let result = || match self {
Output::Constant(hash, Some(output)) => {
match output.to_fields() {
Ok(fields) => {
let index = Field::from_u16(index as u16);
let mut preimage = Vec::new();
preimage.push(function_id);
preimage.extend(fields);
preimage.push(*tcm);
preimage.push(index);
match N::hash_psd8(&preimage) {
Ok(candidate_hash) => Ok(hash == &candidate_hash),
Err(error) => Err(error),
}
}
Err(error) => Err(error),
}
}
Output::Public(hash, Some(output)) => {
match output.to_fields() {
Ok(fields) => {
let index = Field::from_u16(index as u16);
let mut preimage = Vec::new();
preimage.push(function_id);
preimage.extend(fields);
preimage.push(*tcm);
preimage.push(index);
match N::hash_psd8(&preimage) {
Ok(candidate_hash) => Ok(hash == &candidate_hash),
Err(error) => Err(error),
}
}
Err(error) => Err(error),
}
}
Output::Private(hash, Some(value)) => {
match value.to_fields() {
Ok(fields) => match N::hash_psd8(&fields) {
Ok(candidate_hash) => Ok(hash == &candidate_hash),
Err(error) => Err(error),
},
Err(error) => Err(error),
}
}
Output::Record(_, checksum, Some(record_ciphertext), sender_ciphertext)
| Output::RecordWithDynamicID(_, checksum, Some(record_ciphertext), sender_ciphertext, _) => {
let mut preimage = record_ciphertext.to_bits_le();
if **record_ciphertext.version() == 0 {
ensure!(sender_ciphertext.is_none(), "The sender ciphertext must be None for Version 0 records");
preimage.truncate(preimage.len().saturating_sub(8));
} else if **record_ciphertext.version() == 1 {
ensure!(sender_ciphertext.is_some(), "The sender ciphertext must be non-empty");
ensure!(sender_ciphertext.unwrap() != Field::zero(), "The sender ciphertext must be non-zero");
} else {
bail!(
"The record version must be set to Version 0 or 1, but found Version {}",
**record_ciphertext.version()
);
}
match N::hash_bhp1024(&preimage) {
Ok(candidate_hash) => Ok(checksum == &candidate_hash),
Err(error) => Err(error),
}
}
Output::Future(hash, Some(output)) => {
match output.to_fields() {
Ok(fields) => {
let index = Field::from_u16(index as u16);
let mut preimage = Vec::new();
preimage.push(function_id);
preimage.extend(fields);
preimage.push(*tcm);
preimage.push(index);
match N::hash_psd8(&preimage) {
Ok(candidate_hash) => Ok(hash == &candidate_hash),
Err(error) => Err(error),
}
}
Err(error) => Err(error),
}
}
Output::Constant(_, None)
| Output::Public(_, None)
| Output::Private(_, None)
| Output::Record(_, _, None, _)
| Output::RecordWithDynamicID(_, _, None, _, _)
| Output::Future(_, None) => {
bail!("A transition output value is missing")
}
Output::ExternalRecord(_) => Ok(true),
Output::DynamicRecord(_) => Ok(true),
Output::ExternalRecordWithDynamicID(_, _) => Ok(true),
};
match result() {
Ok(is_hash_valid) => is_hash_valid,
Err(error) => {
eprintln!("{error}");
false
}
}
}
pub fn is_type(&self, expected_value_type: &ValueType<N>) -> bool {
matches!(
(self, expected_value_type),
(Self::Constant(..), ValueType::Constant(..))
| (Self::Public(..), ValueType::Public(..))
| (Self::Private(..), ValueType::Private(..))
| (Self::Record(..), ValueType::Record(..))
| (Self::RecordWithDynamicID(..), ValueType::Record(..))
| (Self::ExternalRecord(..), ValueType::ExternalRecord(..))
| (Self::ExternalRecordWithDynamicID(..), ValueType::ExternalRecord(..))
| (Self::Future(..), ValueType::Future(..))
| (Self::DynamicRecord(..), ValueType::DynamicRecord)
)
}
}
#[cfg(test)]
pub(crate) mod test_helpers {
use super::*;
use console::{network::MainnetV0, program::Literal};
type CurrentNetwork = MainnetV0;
pub(crate) fn sample_outputs() -> Vec<(<CurrentNetwork as Network>::TransitionID, Output<CurrentNetwork>)> {
let rng = &mut TestRng::default();
let transaction = crate::transaction::test_helpers::sample_execution_transaction_with_fee(true, rng, 0);
let transition = transaction.transitions().next().unwrap();
let transition_id = *transition.id();
let input = transition.outputs().iter().next().unwrap().clone();
let plaintext = Plaintext::Literal(Literal::Field(Uniform::rand(rng)), Default::default());
let plaintext_hash = CurrentNetwork::hash_bhp1024(&plaintext.to_bits_le()).unwrap();
let fields: Vec<_> = (0..10).map(|_| Uniform::rand(rng)).collect();
let ciphertext = Ciphertext::from_fields(&fields).unwrap();
let ciphertext_hash = CurrentNetwork::hash_bhp1024(&ciphertext.to_bits_le()).unwrap();
let randomizer = Uniform::rand(rng);
let nonce = CurrentNetwork::g_scalar_multiply(&randomizer);
let record = Record::<CurrentNetwork, Plaintext<CurrentNetwork>>::from_str(
&format!("{{ owner: aleo1d5hg2z3ma00382pngntdp68e74zv54jdxy249qhaujhks9c72yrs33ddah.private, token_amount: 100u64.private, _nonce: {nonce}.public }}"),
).unwrap();
let record_ciphertext = record.encrypt(randomizer).unwrap();
let record_checksum = CurrentNetwork::hash_bhp1024(&record_ciphertext.to_bits_le()).unwrap();
let sender_ciphertext = match record_ciphertext.version().is_zero() {
true => None,
false => Some(Uniform::rand(rng)),
};
vec![
(transition_id, input),
(Uniform::rand(rng), Output::Constant(Uniform::rand(rng), None)),
(Uniform::rand(rng), Output::Constant(plaintext_hash, Some(plaintext.clone()))),
(Uniform::rand(rng), Output::Public(Uniform::rand(rng), None)),
(Uniform::rand(rng), Output::Public(plaintext_hash, Some(plaintext))),
(Uniform::rand(rng), Output::Private(Uniform::rand(rng), None)),
(Uniform::rand(rng), Output::Private(ciphertext_hash, Some(ciphertext))),
(Uniform::rand(rng), Output::Record(Uniform::rand(rng), Uniform::rand(rng), None, sender_ciphertext)),
(
Uniform::rand(rng),
Output::Record(Uniform::rand(rng), record_checksum, Some(record_ciphertext.clone()), sender_ciphertext),
),
(Uniform::rand(rng), Output::ExternalRecord(Uniform::rand(rng))),
(
Uniform::rand(rng),
Output::RecordWithDynamicID(
Uniform::rand(rng),
record_checksum,
Some(record_ciphertext),
sender_ciphertext,
Uniform::rand(rng),
),
),
(
Uniform::rand(rng),
Output::RecordWithDynamicID(
Uniform::rand(rng),
Uniform::rand(rng),
None,
sender_ciphertext,
Uniform::rand(rng),
),
),
(Uniform::rand(rng), Output::ExternalRecordWithDynamicID(Uniform::rand(rng), Uniform::rand(rng))),
(Uniform::rand(rng), Output::DynamicRecord(Uniform::rand(rng))),
]
}
}
#[cfg(test)]
mod tests {
use super::*;
use console::network::MainnetV0;
type CurrentNetwork = MainnetV0;
#[test]
fn test_to_caller_output_record_with_dynamic_id() {
let commitment = Field::<CurrentNetwork>::from_u64(1);
let checksum = Field::<CurrentNetwork>::from_u64(2);
let dynamic_id = Field::<CurrentNetwork>::from_u64(3);
let output = Output::<CurrentNetwork>::RecordWithDynamicID(commitment, checksum, None, None, dynamic_id);
let caller_output = output.to_caller_output();
assert_eq!(caller_output, Output::<CurrentNetwork>::DynamicRecord(dynamic_id));
}
#[test]
fn test_to_caller_output_external_record_with_dynamic_id() {
let ext_id = Field::<CurrentNetwork>::from_u64(10);
let dynamic_id = Field::<CurrentNetwork>::from_u64(20);
let output = Output::<CurrentNetwork>::ExternalRecordWithDynamicID(ext_id, dynamic_id);
let caller_output = output.to_caller_output();
assert_eq!(caller_output, Output::<CurrentNetwork>::DynamicRecord(dynamic_id));
}
#[test]
fn test_to_caller_output_non_dynamic_variants_unchanged() {
let id = Field::<CurrentNetwork>::from_u64(42);
let constant = Output::<CurrentNetwork>::Constant(id, None);
assert_eq!(constant.to_caller_output(), constant);
let dynamic_record = Output::<CurrentNetwork>::DynamicRecord(id);
assert_eq!(dynamic_record.to_caller_output(), dynamic_record);
let external = Output::<CurrentNetwork>::ExternalRecord(id);
assert_eq!(external.to_caller_output(), external);
}
}