Skip to content

Commit

Permalink
Merge branch 'main' into feat/refactor-local-state
Browse files Browse the repository at this point in the history
  • Loading branch information
scarmuega committed Nov 9, 2023
2 parents e2ac5b8 + aae7d92 commit 0481999
Show file tree
Hide file tree
Showing 28 changed files with 3,109 additions and 187 deletions.
4 changes: 4 additions & 0 deletions pallas-applying/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@ authors = ["Maico Leberle <[email protected]>"]
doctest = false

[dependencies]
pallas-addresses = { path = "../pallas-addresses" }
pallas-codec = { path = "../pallas-codec" }
pallas-crypto = { path = "../pallas-crypto" }
pallas-primitives = { path = "../pallas-primitives" }
pallas-traverse = { path = "../pallas-traverse" }
rand = "0.8"

[dev-dependencies]
hex = "0.4"
1 change: 0 additions & 1 deletion pallas-applying/docs/byron-validation-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Refer to the [Byron's ledger white paper](https://github.com/input-output-hk/car
- ***fees: Tx → ℕ*** gives the fees paid by a transaction, defined as follows:
- ***fees(tx) := balance (txIns(tx) ◁ utxo) − balance (txOuts(tx))***, where
- ***balance : P(TxOut) → ℕ*** gives the summation of all the lovelaces in a set of transaction outputs.

- **Serialization**:
- ***Bytes*** is the set of byte arrays (a.k.a. data, upon which signatures are built).
- ***⟦_⟧<sub>A</sub> : A -> Bytes*** takes an element of type ***A*** and returns a byte array resulting from serializing it.
Expand Down
251 changes: 245 additions & 6 deletions pallas-applying/src/byron.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,253 @@
//! Utilities required for Byron-era transaction validation.
use crate::types::{ByronProtParams, UTxOs, ValidationResult};
use std::borrow::Cow;

use pallas_primitives::byron::MintedTxPayload;
use crate::types::{
ByronProtParams, MultiEraInput, MultiEraOutput, SigningTag, UTxOs, ValidationError,
ValidationResult,
};

use pallas_addresses::byron::{
AddrAttrs, AddrType, AddressId, AddressPayload, ByronAddress, SpendingData,
};
use pallas_codec::{
minicbor::{encode, Encoder},
utils::CborWrap,
};
use pallas_crypto::{
hash::Hash,
key::ed25519::{PublicKey, Signature},
};
use pallas_primitives::byron::{
Address, MintedTxPayload, PubKey, Signature as ByronSignature, Twit, Tx, TxIn, TxOut,
};
use pallas_traverse::OriginalHash;

// TODO: implement each of the validation rules.
pub fn validate_byron_tx(
_mtxp: &MintedTxPayload,
_utxos: &UTxOs,
_prot_pps: &ByronProtParams,
mtxp: &MintedTxPayload,
utxos: &UTxOs,
prot_pps: &ByronProtParams,
prot_magic: &u32,
) -> ValidationResult {
let tx: &Tx = &mtxp.transaction;
let size: u64 = get_tx_size(tx)?;
check_ins_not_empty(tx)?;
check_outs_not_empty(tx)?;
check_ins_in_utxos(tx, utxos)?;
check_outs_have_lovelace(tx)?;
check_fees(tx, &size, utxos, prot_pps)?;
check_size(&size, prot_pps)?;
check_witnesses(mtxp, utxos, prot_magic)
}

fn check_ins_not_empty(tx: &Tx) -> ValidationResult {
if tx.inputs.clone().to_vec().is_empty() {
return Err(ValidationError::TxInsEmpty);
}
Ok(())
}

fn check_outs_not_empty(tx: &Tx) -> ValidationResult {
if tx.outputs.clone().to_vec().is_empty() {
return Err(ValidationError::TxOutsEmpty);
}
Ok(())
}

fn check_ins_in_utxos(tx: &Tx, utxos: &UTxOs) -> ValidationResult {
for input in tx.inputs.iter() {
if !(utxos.contains_key(&MultiEraInput::from_byron(input))) {
return Err(ValidationError::InputMissingInUTxO);
}
}
Ok(())
}

fn check_outs_have_lovelace(tx: &Tx) -> ValidationResult {
for output in tx.outputs.iter() {
if output.amount == 0 {
return Err(ValidationError::OutputWithoutLovelace);
}
}
Ok(())
}

fn check_fees(tx: &Tx, size: &u64, utxos: &UTxOs, prot_pps: &ByronProtParams) -> ValidationResult {
let mut inputs_balance: u64 = 0;
for input in tx.inputs.iter() {
match utxos
.get(&MultiEraInput::from_byron(input))
.and_then(MultiEraOutput::as_byron)
{
Some(byron_utxo) => inputs_balance += byron_utxo.amount,
None => return Err(ValidationError::UnableToComputeFees),
}
}
let mut outputs_balance: u64 = 0;
for output in tx.outputs.iter() {
outputs_balance += output.amount
}
let total_balance: u64 = inputs_balance - outputs_balance;
let min_fees: u64 = prot_pps.min_fees_const + prot_pps.min_fees_factor * size;
if total_balance < min_fees {
return Err(ValidationError::FeesBelowMin);
}
Ok(())
}

fn check_size(size: &u64, prot_pps: &ByronProtParams) -> ValidationResult {
if *size > prot_pps.max_tx_size {
return Err(ValidationError::MaxTxSizeExceeded);
}
Ok(())
}

fn get_tx_size(tx: &Tx) -> Result<u64, ValidationError> {
let mut buff: Vec<u8> = Vec::new();
match encode(tx, &mut buff) {
Ok(()) => Ok(buff.len() as u64),
Err(_) => Err(ValidationError::UnknownTxSize),
}
}

pub enum TaggedSignature<'a> {
PkWitness(&'a ByronSignature),
RedeemWitness(&'a ByronSignature),
}

fn check_witnesses(mtxp: &MintedTxPayload, utxos: &UTxOs, prot_magic: &u32) -> ValidationResult {
let tx: &Tx = &mtxp.transaction;
let tx_hash: Hash<32> = mtxp.transaction.original_hash();
let witnesses: Vec<(&PubKey, TaggedSignature)> = tag_witnesses(&mtxp.witness)?;
let tx_inputs: &Vec<TxIn> = &tx.inputs;
for input in tx_inputs {
let tx_out: &TxOut = find_tx_out(input, utxos)?;
let (pub_key, sign): (&PubKey, &TaggedSignature) = find_raw_witness(tx_out, &witnesses)?;
let public_key: PublicKey = get_verification_key(pub_key);
let data_to_verify: Vec<u8> = get_data_to_verify(sign, prot_magic, &tx_hash)?;
let signature: Signature = get_signature(sign);
if !public_key.verify(data_to_verify, &signature) {
return Err(ValidationError::WrongSignature);
}
}
Ok(())
}

fn tag_witnesses(wits: &[Twit]) -> Result<Vec<(&PubKey, TaggedSignature)>, ValidationError> {
let mut res: Vec<(&PubKey, TaggedSignature)> = Vec::new();
for wit in wits.iter() {
match wit {
Twit::PkWitness(CborWrap((pk, sig))) => {
res.push((pk, TaggedSignature::PkWitness(sig)));
}
Twit::RedeemWitness(CborWrap((pk, sig))) => {
res.push((pk, TaggedSignature::RedeemWitness(sig)));
}
_ => return Err(ValidationError::UnableToProcessWitnesses),
}
}
Ok(res)
}

fn find_tx_out<'a>(input: &'a TxIn, utxos: &'a UTxOs) -> Result<&'a TxOut, ValidationError> {
let key: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Borrowed(input)));
utxos
.get(&key)
.ok_or(ValidationError::InputMissingInUTxO)?
.as_byron()
.ok_or(ValidationError::InputMissingInUTxO)
}

fn find_raw_witness<'a>(
tx_out: &TxOut,
witnesses: &'a Vec<(&'a PubKey, TaggedSignature<'a>)>,
) -> Result<(&'a PubKey, &'a TaggedSignature<'a>), ValidationError> {
let address: ByronAddress = mk_byron_address(&tx_out.address);
let addr_payload: AddressPayload = address
.decode()
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
let root: AddressId = addr_payload.root;
let attr: AddrAttrs = addr_payload.attributes;
let addr_type: AddrType = addr_payload.addrtype;
for (pub_key, sign) in witnesses {
if redeems(pub_key, sign, &root, &attr, &addr_type) {
match addr_type {
AddrType::PubKey | AddrType::Redeem => return Ok((pub_key, sign)),
_ => return Err(ValidationError::UnableToProcessWitnesses),
}
}
}
Err(ValidationError::MissingWitness)
}

fn mk_byron_address(addr: &Address) -> ByronAddress {
ByronAddress::new((*addr.payload.0).as_slice(), addr.crc)
}

fn redeems(
pub_key: &PubKey,
sign: &TaggedSignature,
root: &AddressId,
attrs: &AddrAttrs,
addr_type: &AddrType,
) -> bool {
let spending_data: SpendingData = mk_spending_data(pub_key, addr_type);
let hash_to_check: AddressId =
AddressPayload::hash_address_id(addr_type, &spending_data, attrs);
hash_to_check == *root && convert_to_addr_type(sign) == *addr_type
}

fn convert_to_addr_type(sign: &TaggedSignature) -> AddrType {
match sign {
TaggedSignature::PkWitness(_) => AddrType::PubKey,
TaggedSignature::RedeemWitness(_) => AddrType::Redeem,
}
}

fn mk_spending_data(pub_key: &PubKey, addr_type: &AddrType) -> SpendingData {
match addr_type {
AddrType::PubKey => SpendingData::PubKey(pub_key.clone()),
AddrType::Redeem => SpendingData::Redeem(pub_key.clone()),
_ => unreachable!(),
}
}

fn get_verification_key(pk: &PubKey) -> PublicKey {
let mut trunc_len: [u8; PublicKey::SIZE] = [0; PublicKey::SIZE];
trunc_len.copy_from_slice(&pk.as_slice()[0..PublicKey::SIZE]);
From::<[u8; PublicKey::SIZE]>::from(trunc_len)
}

fn get_data_to_verify(
sign: &TaggedSignature,
prot_magic: &u32,
tx_hash: &Hash<32>,
) -> Result<Vec<u8>, ValidationError> {
let buff: &mut Vec<u8> = &mut Vec::new();
let mut enc: Encoder<&mut Vec<u8>> = Encoder::new(buff);
match sign {
TaggedSignature::PkWitness(_) => {
enc.encode(SigningTag::Tx as u64)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
}
TaggedSignature::RedeemWitness(_) => {
enc.encode(SigningTag::RedeemTx as u64)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
}
}
enc.encode(prot_magic)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
enc.encode(tx_hash)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
Ok(enc.into_writer().clone())
}

fn get_signature(tagged_signature: &TaggedSignature<'_>) -> Signature {
let inner_sig = match tagged_signature {
TaggedSignature::PkWitness(sign) => sign,
TaggedSignature::RedeemWitness(sign) => sign,
};
let mut trunc_len: [u8; Signature::SIZE] = [0; Signature::SIZE];
trunc_len.copy_from_slice(inner_sig.as_slice());
From::<[u8; Signature::SIZE]>::from(trunc_len)
}
20 changes: 10 additions & 10 deletions pallas-applying/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ use byron::validate_byron_tx;

use pallas_traverse::{MultiEraTx, MultiEraTx::Byron as ByronTxPayload};

pub use types::{
MultiEraProtParams, MultiEraProtParams::Byron as ByronProtParams, UTxOs, ValidationResult,
};
pub use types::{Environment, MultiEraProtParams, UTxOs, ValidationResult};

pub fn validate(
metx: &MultiEraTx,
utxos: &UTxOs,
prot_pps: &MultiEraProtParams,
) -> ValidationResult {
match (metx, prot_pps) {
(ByronTxPayload(mtxp), ByronProtParams(bpp)) => validate_byron_tx(mtxp, utxos, bpp),
pub fn validate(metx: &MultiEraTx, utxos: &UTxOs, env: &Environment) -> ValidationResult {
match (metx, env) {
(
ByronTxPayload(mtxp),
Environment {
prot_params: MultiEraProtParams::Byron(bpp),
prot_magic,
},
) => validate_byron_tx(mtxp, utxos, bpp, prot_magic),
// TODO: implement the rest of the eras.
_ => Ok(()),
}
Expand Down
38 changes: 31 additions & 7 deletions pallas-applying/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,51 @@
//! Base types used for validating transactions in each era.
use std::{borrow::Cow, collections::HashMap};
use std::collections::HashMap;

pub use pallas_traverse::{MultiEraInput, MultiEraOutput};

pub type UTxOs<'b> = HashMap<MultiEraInput<'b>, MultiEraOutput<'b>>;

// TODO: add a field for each protocol parameter in the Byron era.
#[derive(Debug, Clone)]
pub struct ByronProtParams;
pub struct ByronProtParams {
pub min_fees_const: u64,
pub min_fees_factor: u64,
pub max_tx_size: u64,
}

// TODO: add variants for the other eras.
#[derive(Debug)]
#[non_exhaustive]
pub enum MultiEraProtParams<'b> {
Byron(Box<Cow<'b, ByronProtParams>>),
pub enum MultiEraProtParams {
Byron(ByronProtParams),
}

#[derive(Debug)]
pub struct Environment {
pub prot_params: MultiEraProtParams,
pub prot_magic: u32,
}

#[non_exhaustive]
pub enum SigningTag {
Tx = 0x01,
RedeemTx = 0x02,
}

// TODO: replace this generic variant with validation-rule-specific ones.
#[derive(Debug)]
#[non_exhaustive]
pub enum ValidationError {
ValidationError,
InputMissingInUTxO,
TxInsEmpty,
TxOutsEmpty,
OutputWithoutLovelace,
UnknownTxSize,
UnableToComputeFees,
FeesBelowMin,
MaxTxSizeExceeded,
UnableToProcessWitnesses,
MissingWitness,
WrongSignature,
}

pub type ValidationResult = Result<(), ValidationError>;
20 changes: 20 additions & 0 deletions pallas-applying/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Testing framework documentation

## Execution
Starting at the root of the repository, simply go to *pallas-applying* and run `cargo test`.


## Explanations
*pallas-applying/tests/byron.rs* contains multiple unit tests for validation on the Byron era.

The first one, **suceessful_mainnet_tx**, is a positive unit test. It takes the CBOR of a mainnet transaction. Namely, the one whose hash is `a06e5a0150e09f8983be2deafab9e04afc60d92e7110999eb672c903343f1e26`, which can be viewed on Cardano Explorer [here](https://cexplorer.io/tx/a06e5a0150e09f8983be2deafab9e04afc60d92e7110999eb672c903343f1e26). Such a transaction has a single input which is added to the UTxO, prior to validation, by associating it to a transaction output sitting at its real (mainnet) address. This information was taken from Cardano Explorer as well, following the address link of the only input to the transaction, and taking its raw address CBOR content.

Then comes a series of negative unit tests, namely:
- **empty_ins** takes the mainnet transaction, removes its input, and calls validation on it.
- **empty_outs** is analogous to the **empty_ins** test, removing all outputs instead.
- **unfound_utxo** takes the mainnet transaction and calls validation on it without a proper UTxO containing an entry for its input.
- **output_without_lovelace** takes the mainnet transaction and modifies its output by removing all of its lovelace.
- **not_enough_fees** takes the mainnet transaction and calls validation on it using wrong protocol parameters, which requiere that the transaction pay a higher fee than the one actually paid.
- **tx_size_exceeds_max** takes the mainnet transaction and calls validation on it using wrong protocol parameters, which only allow transactions of a size smaller than that of the transaction.
- **missing_witness** takes the mainnet transaction, removes its witness, and calls validation on it.
- **wrong_signature** takes the mainnet transaction, alters the content of its witness, and calls validation on it.
Loading

0 comments on commit 0481999

Please sign in to comment.