mod bytes;
mod serialize;
mod string;
use console::{network::prelude::*, program::Request, types::Field};
use ledger_block::{Transaction, Transition};
use synthesizer_program::StackProgram;
use indexmap::IndexMap;
#[cfg(feature = "locktick")]
use locktick::parking_lot::RwLock;
#[cfg(not(feature = "locktick"))]
use parking_lot::RwLock;
use std::{
collections::{HashMap, VecDeque},
sync::Arc,
};
#[derive(Clone)]
pub struct Authorization<N: Network> {
requests: Arc<RwLock<VecDeque<Request<N>>>>,
transitions: Arc<RwLock<IndexMap<N::TransitionID, Transition<N>>>>,
}
impl<N: Network> Authorization<N> {
pub fn new(request: Request<N>) -> Self {
Self { requests: Arc::new(RwLock::new(VecDeque::from(vec![request]))), transitions: Default::default() }
}
pub fn replicate(&self) -> Self {
Self {
requests: Arc::new(RwLock::new(self.requests.read().clone())),
transitions: Arc::new(RwLock::new(self.transitions.read().clone())),
}
}
}
impl<N: Network> TryFrom<(Vec<Request<N>>, Vec<Transition<N>>)> for Authorization<N> {
type Error = Error;
fn try_from((requests, transitions): (Vec<Request<N>>, Vec<Transition<N>>)) -> Result<Self> {
ensure!(
requests.len() == transitions.len(),
"The number of requests ({}) and transitions ({}) must match in the authorization.",
requests.len(),
transitions.len()
);
let tcm_indices: HashMap<_, _> = requests.iter().enumerate().map(|(i, request)| (request.tcm(), i)).collect();
for (index, transition) in transitions.iter().enumerate() {
let request_idx = tcm_indices
.get(&transition.tcm())
.copied()
.ok_or_else(|| anyhow!("Missing request for transition {}", transition.id()))?;
ensure_request_and_transition_matches(index, &requests[request_idx], transition)?;
}
Ok(Self {
requests: Arc::new(RwLock::new(VecDeque::from(requests))),
transitions: Arc::new(RwLock::new(IndexMap::from_iter(
transitions.into_iter().map(|transition| (*transition.id(), transition)),
))),
})
}
}
impl<N: Network> Authorization<N> {
pub fn is_fee_private(&self) -> bool {
let requests = self.requests.read();
match requests.len() {
1 => {
let program_id = requests[0].program_id().to_string();
let function_name = requests[0].function_name().to_string();
&program_id == "credits.aleo" && &function_name == "fee_private"
}
_ => false,
}
}
pub fn is_fee_public(&self) -> bool {
let requests = self.requests.read();
match requests.len() {
1 => {
let program_id = requests[0].program_id().to_string();
let function_name = requests[0].function_name().to_string();
&program_id == "credits.aleo" && &function_name == "fee_public"
}
_ => false,
}
}
pub fn is_split(&self) -> bool {
let requests = self.requests.read();
match requests.len() {
1 => {
let program_id = requests[0].program_id().to_string();
let function_name = requests[0].function_name().to_string();
&program_id == "credits.aleo" && &function_name == "split"
}
_ => false,
}
}
pub fn is_upgrade(&self) -> bool {
let requests = self.requests.read();
match requests.len() {
1 => {
let program_id = requests[0].program_id().to_string();
let function_name = requests[0].function_name().to_string();
&program_id == "credits.aleo" && &function_name == "upgrade"
}
_ => false,
}
}
pub fn check_valid_edition(&self, process: &crate::Process<N>, _consensus_version: ConsensusVersion) -> Result<()> {
let transitions = self.transitions.read();
let program_ids = transitions.iter().map(|(_, t)| t.program_id());
for program_id in program_ids {
if program_id.to_string() != "credits.aleo" {
let _program_edition = *process.get_stack(program_id)?.program_edition();
#[cfg(not(any(test, feature = "test")))]
if _consensus_version >= ConsensusVersion::V8 && _program_edition == 0 {
bail!("Cannot execute {program_id} on edition {_program_edition}");
}
}
}
Ok(())
}
pub fn check_valid_records(&self, consensus_version: ConsensusVersion) -> Result<()> {
let transitions = self.transitions.read();
let output_records = transitions
.values()
.flat_map(|transition| transition.outputs().iter().filter_map(|output| output.record()));
for (_, record) in output_records {
if consensus_version >= ConsensusVersion::V8 {
ensure!(record.version().is_one(), "Output record must be Version 1 on or after Consensus V8");
}
}
Ok(())
}
}
impl<N: Network> Authorization<N> {
pub fn peek_next(&self) -> Result<Request<N>> {
self.requests.read().front().cloned().ok_or_else(|| anyhow!("Failed to peek at the next request."))
}
pub fn next(&self) -> Result<Request<N>> {
self.requests.write().pop_front().ok_or_else(|| anyhow!("No more requests in the authorization."))
}
pub fn get(&self, index: usize) -> Result<Request<N>> {
self.requests.read().get(index).cloned().ok_or_else(|| anyhow!("Attempted to get missing request {index}."))
}
pub fn len(&self) -> usize {
self.requests.read().len()
}
pub fn is_empty(&self) -> bool {
self.requests.read().is_empty()
}
pub fn push(&self, request: Request<N>) -> Result<()> {
ensure!(
self.len() < Transaction::<N>::MAX_TRANSITIONS,
"The number of requests in the authorization must be less than '{}'.",
Transaction::<N>::MAX_TRANSITIONS
);
self.requests.write().push_back(request);
Ok(())
}
pub fn to_vec_deque(&self) -> VecDeque<Request<N>> {
self.requests.read().clone()
}
}
impl<N: Network> Authorization<N> {
pub fn insert_transition(&self, transition: Transition<N>) -> Result<()> {
ensure!(
!self.transitions.read().contains_key(transition.id()),
"Transition {} is already in the authorization.",
transition.id()
);
self.transitions.write().insert(*transition.id(), transition);
Ok(())
}
pub fn transitions(&self) -> IndexMap<N::TransitionID, Transition<N>> {
self.transitions.read().clone()
}
pub fn to_execution_id(&self) -> Result<Field<N>> {
let transitions = self.transitions.read();
if transitions.is_empty() {
bail!("Cannot compute the execution ID for an empty authorization.");
}
Ok(*Transaction::transitions_tree(transitions.values())?.root())
}
}
impl<N: Network> PartialEq for Authorization<N> {
fn eq(&self, other: &Self) -> bool {
let self_requests = self.requests.read();
let other_requests = other.requests.read();
let self_transitions = self.transitions.read();
let other_transitions = other.transitions.read();
*self_requests == *other_requests && *self_transitions == *other_transitions
}
}
impl<N: Network> Eq for Authorization<N> {}
fn ensure_request_and_transition_matches<N: Network>(
index: usize,
request: &Request<N>,
transition: &Transition<N>,
) -> Result<()> {
ensure!(
request.program_id() == transition.program_id(),
"The request ({}) and transition ({}) at index {index} must have the same program ID in the authorization.",
request.program_id(),
transition.program_id(),
);
ensure!(
request.function_name() == transition.function_name(),
"The request ({}) and transition ({}) at index {index} must have the same function name in the authorization.",
request.function_name(),
transition.function_name(),
);
ensure!(
request.input_ids().len() == transition.input_ids().len(),
"The request ({}) and transition ({}) at index {index} must have the same number of inputs in the authorization.",
request.input_ids().len(),
transition.input_ids().len(),
);
ensure!(
request.to_tpk() == *transition.tpk(),
"The request ({}) and transition ({}) at index {index} must have the same 'tpk' in the authorization.",
request.to_tpk(),
*transition.tpk(),
);
ensure!(
request.tcm() == transition.tcm(),
"The request ({}) and transition ({}) at index {index} must have the same 'tcm' in the authorization.",
request.tcm(),
transition.tcm(),
);
ensure!(
request.scm() == transition.scm(),
"The request ({}) and transition ({}) at index {index} must have the same 'scm' in the authorization.",
request.scm(),
transition.scm(),
);
Ok(())
}
#[cfg(test)]
pub(crate) mod test_helpers {
use super::*;
use crate::{Identifier, Process, ProgramID, Value};
use console::account::{Address, PrivateKey};
type CurrentNetwork = console::network::MainnetV0;
type CurrentAleo = circuit::AleoV0;
pub fn sample_authorization(rng: &mut TestRng) -> Authorization<CurrentNetwork> {
let process = Process::<CurrentNetwork>::load().unwrap();
let private_key = PrivateKey::new(rng).unwrap();
let base_fee_in_microcredits = rng.gen_range(1_000_000..u64::MAX / 2);
let priority_fee_in_microcredits = rng.gen_range(0..u64::MAX / 2);
let deployment_or_execution_id = Field::rand(rng);
let authorization = process
.authorize_fee_public::<CurrentAleo, _>(
&private_key,
base_fee_in_microcredits,
priority_fee_in_microcredits,
deployment_or_execution_id,
rng,
)
.unwrap();
assert!(authorization.is_fee_public(), "Authorization must be for a call to 'credits.aleo/fee_public'");
authorization
}
#[test]
fn test_single_transition_authorization_deserialization() {
let rng = &mut TestRng::default();
let private_key = PrivateKey::new(rng).unwrap();
let process = Process::<CurrentNetwork>::load().unwrap();
let program_id = ProgramID::<CurrentNetwork>::from_str("credits.aleo").unwrap();
let function_name = Identifier::<CurrentNetwork>::from_str("transfer_public").unwrap();
let destination =
Value::<CurrentNetwork>::from_str(&format!("{}", Address::try_from(private_key).unwrap())).unwrap();
let amount = Value::<CurrentNetwork>::from_str("1u64").unwrap();
let authorization = process
.authorize::<CurrentAleo, _>(
&private_key,
&program_id,
&function_name,
vec![destination, amount].iter(),
rng,
)
.unwrap();
assert!(authorization.transitions().len() == 1);
let authorization_serialized = authorization.to_string();
let deserialization_result = Authorization::<CurrentNetwork>::from_str(&authorization_serialized);
assert!(deserialization_result.is_ok());
}
}