Skip to content

Commit

Permalink
feat(applying): implement ShelleyMA phase-1 validations (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaicoLeberle authored Dec 12, 2023
1 parent 472692c commit 04232c6
Show file tree
Hide file tree
Showing 15 changed files with 1,640 additions and 206 deletions.
108 changes: 108 additions & 0 deletions pallas-applying/docs/shelleyMA-validation-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# ShelleyMA transaction validation rules

This document covers the Shelley era, including its Allegra and Mary hard forks. We write *ShelleyMA* to refer to any of these ledger versions, and *Shelley*, *Allegra* or *Mary* when the discrimination is relevant. This document covers only the concepts, notation and validation rules realted to phase-1 validation in these ledger versions. For further information, refer to the corresponding white paper listed below:
- [Shelley's ledger white paper](https://github.com/input-output-hk/cardano-ledger/releases/latest/download/shelley-ledger.pdf)
- [Both Allegra's and Mary's ledger white paper](https://github.com/input-output-hk/cardano-ledger/releases/latest/download/mary-ledger.pdf)

## Definitions and notation
- **Scripts**:
- ***Script*** is the set of all possible native scripts.
- **Transactions**:
- ***Tx*** is the set of ShelleyMA transactions, composed of a transaction body and the set of witnesses.
- ***TxBody*** is the type of ShelleyMA transaction bodies. Each transaction body is composed of a set of inputs and a list of outputs.
- ***txBody(tx)*** is the transaction body of the transaction.
- ***TxOut = Addr x TA*** is the set of transaction outputs, where
- ***Addr*** is the set of transaction output addresses.
- ***TA = ℕ*** in Shelley and Allegra, while ***TA = Value*** in Mary, where ***Value*** is the type of multi-asset Mary values.
- ***txOuts(txBody) ∈ P(TxOut)*** gives the set of transaction outputs of a transaction body.
- ***balance : P(TxOut) → TA*** gives the sum of all lovelaces in a set of transaction outputs in Shelley and Allegra, while it gives the sum of all assets in a set of transaction outputs in Mary. That is, ***TA = ℕ*** in Shelley and Allegra, and ***TA = Value*** in Mary.
- ***TxIn = TxId x Ix*** is the set of transaction inputs, where
- ***TxId*** is the set of transaction IDs.
- ***Ix = ℕ*** is the set of indices (used to refer to a specific transaction output).
- ***txIns(txBody) ∈ P(TxIn)*** gives the set of transaction inputs of the transaction.
- ***utxo : TxIn → TxOut*** is a (partial) map that gives the unspent transaction output (UTxO) associated with a transaction input.
- Given ***A ⊆ dom(utxo)***, we will write ***A ◁ utxo := {to ∈ TxOut / ∃ ti ∈ dom utxo: utxo(ti) = to}***. Thus, we will write ***txIns(tx) ◁ utxo := {to ∈ TxOut / ∃ ti ∈ dom(utxo): utxo(ti) = to}*** to express the set of unspent transaction outputs associated with the set of transaction inputs of the transaction ***tx***.
- ***txTTL(txBody) ∈ Slot*** is the time-to-live of the transaction.
- ***txSize(Tx) ∈ ℕ*** gives the size of the transaction.
- ***fee(txBody) ∈ ℕ*** gives the fee paid by a transaction.
- ***minted(txBody)*** is the multiasset value minted (or burned) in the transaction.
- ***txInsScript(txBody) ⊆ P(TxIn)*** is the list of script inputs in the transaction body.
- ***consumed(pps, utxo, txBody) ∈ ℤ*** is the *consumed value* of the transaction.
- In Shelley and Allegra, this equals the sum of all lovelace in the transaction inputs.
- In Mary, this equals the sum of all multiasset values in the transaction inputs.
- ***produced(pps, txBody) ∈ ℤ*** is the *produced value* of the transaction.
- In Shelley and Allegra, this equals the sum of all lovelace in the transaction outputs plus the transaction fee.
- In Mary, this equals the sum of all multiasset values in the outputs of the transaction plus the transaction fee plus the minted value.
- **Transaction metadata**:
- ***txMD(tx)*** is the metadata of the transaction.
- ***txMDHash(txBody)*** is the metadata hash contained within the transaction body.
- ***hashMD(md)*** is the result of hasing metadata ***md***.
- **Addresses*:
- ***Addr*** is the set of all valid ShelleyMA addresses.
- ***netId(addr)*** is the network ID of the address.
- ***NetworkId*** is the global network ID.
- ***Slots***:
- ***Slot ∈ ℕ*** is the set of slots. When necessary, we write ***slot ∈ Slot*** to refer to the slot associated to the current block.
- **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.
- **Hashing**:
- ***KeyHash ⊆ Bytes*** is the set of fixed-size byte arrays resulting from hashing processes.
- ***hash: Bytes -> KeyHash*** is the hashing function.
- ***paymentCredential<sub>utxo</sub>(txIn) ∈ KeyHash*** gets from ***txIn*** the associated transaction output in ***utxo***, extracts the address contained in it, and returns its hash. In other words, given ***utxo*** and transaction input ***txIn*** such that ***utxo(txIn) = (a, _)***, we have that ***paymentCredential<sub>utxo</sub>(txIn) = hash(a)***.
- **Protocol Parameters**:
- We will write ***pps ∈ PParams*** to represent the set of (ShelleyMA) protocol parameters, with the following associated functions:
- ***minFees(pps, txBody) ∈ ℕ*** gives the minimum number of lovelace that must be paid for the transaction as fee.
- ***maxTxSize(pps) ∈ ℕ*** gives the (global) maximum transaction size.
- ***minUTxOValue(pps) ∈ ℕ***, the global minimum number of lovelace every UTxO must lock.
- ***Witnesses***:
- ***VKey*** is the set of verification keys (a.k.a. public keys).
- ***SKey*** is the set of signing keys (a.k.a. private keys).
- ***Sig*** is the set of signatures (i.e., the result of signing a byte array using a signing key).
- ***sig : SKey x Bytes -> Sig*** is the signing function.
- ***verify : VKey x Sig x Bytes -> Bool*** assesses whether the verification key applied to the signature yields the byte array as expected.
- The assumption is that if ***sk*** and ***vk*** are, respectively, a pair of secret and verification keys associated with one another. Thus, if ***sig(sk, d) = σ***, then it must be that ***verify(vk, σ, d) = true***.
- ***txVKWits(tx) ∈ P(VKey x Sig)*** gives the list of pairs of verification keys and signatures of the transaction.
- ***txScriptWits(tx) ⊆ P(Script)*** is the set of script witnesses of the transaction.

## Validation rules
Let ***tx ∈ Tx*** be a ShelleyMA transaction whose body is ***txBody ∈ TxBody***. ***tx*** is a phase-1 valid transaction if and only if

- **The set of transaction inputs is not empty**:

<code>txIns(txBody) ≠ ∅</code>
- **All transaction inputs are in the set of (yet) unspent transaction outputs**:

<code>txIns(txBody) ⊆ dom(utxo)</code>
- **The TTL limit of the transaction has not been exceeded**:

<code>slot ≥ txTTL(txBody)</code>
- **The transaction size does not exceed the protocol limit**:

<code>txSize(tx) ≤ maxTxSize(pps)</code>
- **All transaction outputs contain Lovelace values not under the minimum**:

<code>∀ (_, c) ∈ txOuts(txBody): minUTxOValue(pps) ≤ c</code>
- **The preservation of value property holds**: Assuming no staking or delegation actions are involved, this property takes one of the two forms below:
- In Shelley and Allegra, the equation for the preservation of value is

<code>consumed(pps, utxo, txBody) = produced(pps, poolParams, txBody) + fee(txBody)</code>,
- In Mary, the equation is:

<code>consumed(pps, utxo, txBody) = produced(pps, poolParams, txBody) + fee(txBody) + minted(txBody) </code>,
- **The fee paid by the transaction has to be greater than or equal to the minimum fee**:

<code>fee(txBody) ≥ minFees(pps, tx)</code>
- **The network ID of each output matches the global network ID**:

<code>∀(_ -> (a, _)) ∈ txOuts(txBody): netId(a) = NetworkId</code>
- **The metadata of the transaction is valid**:

<code>txMDHash(tx) = hashMD(txMD(tx))</code>
- **Verification-key witnesses**: The owner of each transaction input signed the transaction. That is, given transaction ***tx*** with body ***txBody***, then for each ***txIn ∈ txIns(txBody)*** there must exist ***(vk, σ) ∈ txVKWits(tx)*** such that:

- <code>verify(vk, σ, ⟦txBody⟧<sub>TxBody</sub>)</code>
- <code>paymentCredential<sub>utxo</sub>(txIn) = hash(vk)</code>
- **Script witnesses**: Each script address has a corresponding witness:

<code>∀ (script_hash, _) ∈ txInsScript(txBody) ◁ utxo : ∃ script ∈ txScriptWits(tx): hash(script) = script_hash</code>
50 changes: 26 additions & 24 deletions pallas-applying/src/byron.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
use std::borrow::Cow;

use crate::types::{
ByronProtParams, MultiEraInput, MultiEraOutput, SigningTag, UTxOs, ValidationError,
ByronError::*,
ByronProtParams, MultiEraInput, MultiEraOutput, SigningTag, UTxOs,
ValidationError::{self, *},
ValidationResult,
};

Expand All @@ -30,34 +32,34 @@ pub fn validate_byron_tx(
prot_magic: &u32,
) -> ValidationResult {
let tx: &Tx = &mtxp.transaction;
let size: u64 = get_tx_size(tx)?;
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_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);
return Err(Byron(TxInsEmpty));
}
Ok(())
}

fn check_outs_not_empty(tx: &Tx) -> ValidationResult {
if tx.outputs.clone().to_vec().is_empty() {
return Err(ValidationError::TxOutsEmpty);
return Err(Byron(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);
return Err(Byron(InputNotInUTxO));
}
}
Ok(())
Expand All @@ -66,7 +68,7 @@ fn check_ins_in_utxos(tx: &Tx, utxos: &UTxOs) -> ValidationResult {
fn check_outs_have_lovelace(tx: &Tx) -> ValidationResult {
for output in tx.outputs.iter() {
if output.amount == 0 {
return Err(ValidationError::OutputWithoutLovelace);
return Err(Byron(OutputWithoutLovelace));
}
}
Ok(())
Expand All @@ -84,7 +86,7 @@ fn check_fees(tx: &Tx, size: &u64, utxos: &UTxOs, prot_pps: &ByronProtParams) ->
.and_then(MultiEraOutput::as_byron)
{
Some(byron_utxo) => inputs_balance += byron_utxo.amount,
None => return Err(ValidationError::UnableToComputeFees),
None => return Err(Byron(UnableToComputeFees)),
}
}
if only_redeem_utxos {
Expand All @@ -95,9 +97,9 @@ fn check_fees(tx: &Tx, size: &u64, utxos: &UTxOs, prot_pps: &ByronProtParams) ->
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;
let min_fees: u64 = prot_pps.fee_policy.summand + prot_pps.fee_policy.multiplier * size;
if total_balance < min_fees {
Err(ValidationError::FeesBelowMin)
Err(Byron(FeesBelowMin))
} else {
Ok(())
}
Expand All @@ -119,7 +121,7 @@ fn is_redeem_utxo(input: &TxIn, utxos: &UTxOs) -> bool {

fn check_size(size: &u64, prot_pps: &ByronProtParams) -> ValidationResult {
if *size > prot_pps.max_tx_size {
return Err(ValidationError::MaxTxSizeExceeded);
return Err(Byron(MaxTxSizeExceeded));
}
Ok(())
}
Expand All @@ -128,7 +130,7 @@ 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),
Err(_) => Err(Byron(UnknownTxSize)),
}
}

Expand All @@ -149,7 +151,7 @@ fn check_witnesses(mtxp: &MintedTxPayload, utxos: &UTxOs, prot_magic: &u32) -> V
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);
return Err(Byron(WrongSignature));
}
}
Ok(())
Expand All @@ -165,7 +167,7 @@ fn tag_witnesses(wits: &[Twit]) -> Result<Vec<(&PubKey, TaggedSignature)>, Valid
Twit::RedeemWitness(CborWrap((pk, sig))) => {
res.push((pk, TaggedSignature::RedeemWitness(sig)));
}
_ => return Err(ValidationError::UnableToProcessWitnesses),
_ => return Err(Byron(UnableToProcessWitness)),
}
}
Ok(res)
Expand All @@ -175,9 +177,9 @@ fn find_tx_out<'a>(input: &'a TxIn, utxos: &'a UTxOs) -> Result<&'a TxOut, Valid
let key: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Borrowed(input)));
utxos
.get(&key)
.ok_or(ValidationError::InputMissingInUTxO)?
.ok_or(Byron(InputNotInUTxO))?
.as_byron()
.ok_or(ValidationError::InputMissingInUTxO)
.ok_or(Byron(InputNotInUTxO))
}

fn find_raw_witness<'a>(
Expand All @@ -187,19 +189,19 @@ fn find_raw_witness<'a>(
let address: ByronAddress = mk_byron_address(&tx_out.address);
let addr_payload: AddressPayload = address
.decode()
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
.map_err(|_| Byron(UnableToProcessWitness))?;
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),
_ => return Err(Byron(UnableToProcessWitness)),
}
}
}
Err(ValidationError::MissingWitness)
Err(Byron(MissingWitness))
}

fn mk_byron_address(addr: &Address) -> ByronAddress {
Expand Down Expand Up @@ -250,17 +252,17 @@ fn get_data_to_verify(
match sign {
TaggedSignature::PkWitness(_) => {
enc.encode(SigningTag::Tx as u64)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
.map_err(|_| Byron(UnableToProcessWitness))?;
}
TaggedSignature::RedeemWitness(_) => {
enc.encode(SigningTag::RedeemTx as u64)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
.map_err(|_| Byron(UnableToProcessWitness))?;
}
}
enc.encode(prot_magic)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
.map_err(|_| Byron(UnableToProcessWitness))?;
enc.encode(tx_hash)
.map_err(|_| ValidationError::UnableToProcessWitnesses)?;
.map_err(|_| Byron(UnableToProcessWitness))?;
Ok(enc.into_writer().clone())
}

Expand Down
41 changes: 29 additions & 12 deletions pallas-applying/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,41 @@
//! Logic for validating and applying new blocks and txs to the chain state
pub mod byron;
pub mod shelley_ma;
pub mod types;

use byron::validate_byron_tx;
use pallas_traverse::{Era, MultiEraTx};
use shelley_ma::validate_shelley_ma_tx;

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

pub use types::{Environment, MultiEraProtParams, UTxOs, ValidationResult};
pub use types::{
Environment, MultiEraProtParams, UTxOs, ValidationError::TxAndProtParamsDiffer,
ValidationResult,
};

pub fn validate(metx: &MultiEraTx, utxos: &UTxOs, env: &Environment) -> ValidationResult {
match (metx, env) {
(
ByronTxPayload(mtxp),
Environment {
prot_params: MultiEraProtParams::Byron(bpp),
prot_magic,
match env {
Environment {
prot_params: MultiEraProtParams::Byron(bpp),
prot_magic,
..
} => match metx {
MultiEraTx::Byron(mtxp) => validate_byron_tx(mtxp, utxos, bpp, prot_magic),
_ => Err(TxAndProtParamsDiffer),
},
Environment {
prot_params: MultiEraProtParams::Shelley(spp),
block_slot,
network_id,
..
} => match metx.era() {
Era::Shelley | Era::Allegra | Era::Mary => match metx.as_alonzo() {
Some(mtx) => {
validate_shelley_ma_tx(mtx, utxos, spp, block_slot, network_id, &metx.era())
}
None => Err(TxAndProtParamsDiffer),
},
) => validate_byron_tx(mtxp, utxos, bpp, prot_magic),
// TODO: implement the rest of the eras.
_ => Ok(()),
_ => Err(TxAndProtParamsDiffer),
},
}
}
Loading

0 comments on commit 04232c6

Please sign in to comment.