diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 8d1bdb20..4a9c7680 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -3,13 +3,19 @@ on: push: {} - name: Validate jobs: check: name: Check - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [windows-latest, ubuntu-latest, macOS-latest] + rust: [stable] + + runs-on: ${{ matrix.os }} + steps: - name: Checkout sources uses: actions/checkout@v2 @@ -18,7 +24,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: ${{ matrix.rust }} override: true - name: Run cargo check @@ -70,4 +76,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: -- -D warnings \ No newline at end of file + args: -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index 537f5d3d..c54da963 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,19 @@ [workspace] - +resolver = "2" members = [ - "pallas-codec", - "pallas-addresses", - "pallas-network", - "pallas-crypto", - "pallas-primitives", - "pallas-traverse", - "pallas-utxorpc", - "pallas", - "examples/block-download", - "examples/block-decode", - "examples/n2n-miniprotocols", - "examples/n2c-miniprotocols", + "pallas-applying", + "pallas-codec", + "pallas-addresses", + "pallas-network", + "pallas-crypto", + "pallas-configs", + "pallas-primitives", + "pallas-rolldb", + "pallas-traverse", + "pallas-utxorpc", + "pallas", + "examples/block-download", + "examples/block-decode", + "examples/n2n-miniprotocols", + "examples/n2c-miniprotocols", ] diff --git a/examples/block-decode/src/main.rs b/examples/block-decode/src/main.rs index c6e9c11a..312f1a05 100644 --- a/examples/block-decode/src/main.rs +++ b/examples/block-decode/src/main.rs @@ -1,7 +1,7 @@ use pallas::ledger::traverse::MultiEraBlock; fn main() { - let blocks = vec![ + let blocks = [ include_str!("blocks/byron.block"), include_str!("blocks/shelley.block"), include_str!("blocks/mary.block"), diff --git a/examples/n2c-miniprotocols/src/main.rs b/examples/n2c-miniprotocols/src/main.rs index 99a07e80..ea253f2e 100644 --- a/examples/n2c-miniprotocols/src/main.rs +++ b/examples/n2c-miniprotocols/src/main.rs @@ -1,19 +1,30 @@ use pallas::network::{ facades::NodeClient, - miniprotocols::{chainsync, localstate, Point, MAINNET_MAGIC}, + miniprotocols::{chainsync, localstate::queries_v16, Point, PRE_PRODUCTION_MAGIC}, }; use tracing::info; async fn do_localstate_query(client: &mut NodeClient) { - client.statequery().acquire(None).await.unwrap(); + let client = client.statequery(); - let result = client - .statequery() - .query(localstate::queries::RequestV10::GetSystemStart) + client.acquire(None).await.unwrap(); + + let result = queries_v16::get_chain_point(client).await.unwrap(); + info!("result: {:?}", result); + + let result = queries_v16::get_system_start(client).await.unwrap(); + info!("result: {:?}", result); + + let era = queries_v16::get_current_era(client).await.unwrap(); + info!("result: {:?}", era); + + let result = queries_v16::get_block_epoch_number(client, era) .await .unwrap(); - info!("system start result: {:?}", result); + info!("result: {:?}", result); + + client.send_release().await.unwrap(); } async fn do_chainsync(client: &mut NodeClient) { @@ -43,7 +54,11 @@ async fn do_chainsync(client: &mut NodeClient) { } } -#[cfg(target_family = "unix")] +// change the following to match the Cardano node socket in your local +// environment +const SOCKET_PATH: &str = "/tmp/node.socket"; + +#[cfg(unix)] #[tokio::main] async fn main() { tracing::subscriber::set_global_default( @@ -55,7 +70,7 @@ async fn main() { // we connect to the unix socket of the local node. Make sure you have the right // path for your environment - let mut client = NodeClient::connect("/tmp/node.socket", MAINNET_MAGIC) + let mut client = NodeClient::connect(SOCKET_PATH, PRE_PRODUCTION_MAGIC) .await .unwrap(); @@ -67,7 +82,6 @@ async fn main() { } #[cfg(not(target_family = "unix"))] - fn main() { panic!("can't use n2c unix socket on non-unix systems"); } diff --git a/pallas-addresses/Cargo.toml b/pallas-addresses/Cargo.toml index 148a0540..97e68d3b 100644 --- a/pallas-addresses/Cargo.toml +++ b/pallas-addresses/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pallas-addresses" description = "Ergonomic library to work with different Cardano addresses" -version = "0.19.0-alpha.1" +version = "0.19.1" edition = "2021" repository = "https://github.com/txpipe/pallas" homepage = "https://github.com/txpipe/pallas" @@ -12,8 +12,10 @@ authors = ["Santiago Carmuega "] [dependencies] hex = "0.4.3" -pallas-crypto = { version = "0.19.0-alpha.0", path = "../pallas-crypto" } -pallas-codec = { version = "0.19.0-alpha.0", path = "../pallas-codec" } +pallas-crypto = { version = "=0.19.1", path = "../pallas-crypto" } +pallas-codec = { version = "=0.19.1", path = "../pallas-codec" } base58 = "0.2.0" bech32 = "0.9.1" thiserror = "1.0.31" +crc = "3.0.1" +sha3 = "0.10.8" diff --git a/pallas-addresses/src/byron.rs b/pallas-addresses/src/byron.rs index 6ce329ba..1d8c2385 100644 --- a/pallas-addresses/src/byron.rs +++ b/pallas-addresses/src/byron.rs @@ -14,8 +14,8 @@ pub type StakeholderId = Blake2b224; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum AddrDistr { - Variant0(StakeholderId), - Variant1, + SingleKeyDistribution(StakeholderId), + BootstrapEraDistribution, } impl<'b, C> minicbor::Decode<'b, C> for AddrDistr { @@ -24,8 +24,8 @@ impl<'b, C> minicbor::Decode<'b, C> for AddrDistr { let variant = d.u32()?; match variant { - 0 => Ok(AddrDistr::Variant0(d.decode_with(ctx)?)), - 1 => Ok(AddrDistr::Variant1), + 0 => Ok(AddrDistr::SingleKeyDistribution(d.decode_with(ctx)?)), + 1 => Ok(AddrDistr::BootstrapEraDistribution), _ => Err(minicbor::decode::Error::message( "invalid variant for addrdstr", )), @@ -40,14 +40,14 @@ impl minicbor::Encode<()> for AddrDistr { _ctx: &mut (), ) -> Result<(), minicbor::encode::Error> { match self { - AddrDistr::Variant0(x) => { + AddrDistr::SingleKeyDistribution(x) => { e.array(2)?; e.u32(0)?; e.encode(x)?; Ok(()) } - AddrDistr::Variant1 => { + AddrDistr::BootstrapEraDistribution => { e.array(1)?; e.u32(1)?; @@ -62,7 +62,7 @@ pub enum AddrType { PubKey, Script, Redeem, - Other(u64), + Other(u32), } impl<'b, C> minicbor::Decode<'b, C> for AddrType { @@ -70,7 +70,7 @@ impl<'b, C> minicbor::Decode<'b, C> for AddrType { d: &mut minicbor::Decoder<'b>, _ctx: &mut C, ) -> Result { - let variant = d.u64()?; + let variant = d.u32()?; match variant { 0 => Ok(AddrType::PubKey), @@ -88,10 +88,10 @@ impl minicbor::Encode for AddrType { _ctx: &mut C, ) -> Result<(), minicbor::encode::Error> { match self { - AddrType::PubKey => e.u64(0)?, - AddrType::Script => e.u64(1)?, - AddrType::Redeem => e.u64(2)?, - AddrType::Other(x) => e.u64(*x)?, + AddrType::PubKey => e.u32(0)?, + AddrType::Script => e.u32(1)?, + AddrType::Redeem => e.u32(2)?, + AddrType::Other(x) => e.u32(*x)?, }; Ok(()) @@ -101,8 +101,8 @@ impl minicbor::Encode for AddrType { #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum AddrAttrProperty { AddrDistr(AddrDistr), - Bytes(ByteVec), - Unparsed(u8, ByteVec), + DerivationPath(ByteVec), + NetworkTag(ByteVec), } impl<'b, C> minicbor::Decode<'b, C> for AddrAttrProperty { @@ -111,8 +111,11 @@ impl<'b, C> minicbor::Decode<'b, C> for AddrAttrProperty { match key { 0 => Ok(AddrAttrProperty::AddrDistr(d.decode_with(ctx)?)), - 1 => Ok(AddrAttrProperty::Bytes(d.decode_with(ctx)?)), - x => Ok(AddrAttrProperty::Unparsed(x, d.decode_with(ctx)?)), + 1 => Ok(AddrAttrProperty::DerivationPath(d.decode_with(ctx)?)), + 2 => Ok(AddrAttrProperty::NetworkTag(d.decode_with(ctx)?)), + _ => Err(minicbor::decode::Error::message( + "unknown tag for address attribute", + )), } } } @@ -125,19 +128,73 @@ impl minicbor::Encode for AddrAttrProperty { ) -> Result<(), minicbor::encode::Error> { match self { AddrAttrProperty::AddrDistr(x) => { - e.u32(0)?; + e.u8(0)?; e.encode(x)?; Ok(()) } - AddrAttrProperty::Bytes(x) => { - e.u32(1)?; + AddrAttrProperty::DerivationPath(x) => { + e.u8(1)?; + e.encode(x)?; + + Ok(()) + } + AddrAttrProperty::NetworkTag(b) => { + e.u8(2)?; + e.encode(b)?; + + Ok(()) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] +pub enum SpendingData { + PubKey(ByteVec), + Script(ByteVec), + Redeem(ByteVec), +} + +impl<'b, C> minicbor::Decode<'b, C> for SpendingData { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + let key = d.u8()?; + + match key { + 0 => Ok(Self::PubKey(d.decode_with(ctx)?)), + 1 => Ok(Self::Script(d.decode_with(ctx)?)), + 2 => Ok(Self::Redeem(d.decode_with(ctx)?)), + _ => Err(minicbor::decode::Error::message( + "unknown tag for spending data", + )), + } + } +} + +impl minicbor::Encode for SpendingData { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + + match self { + Self::PubKey(x) => { + e.u8(0)?; + e.encode(x)?; + + Ok(()) + } + Self::Script(x) => { + e.u8(1)?; e.encode(x)?; Ok(()) } - AddrAttrProperty::Unparsed(a, b) => { - e.encode(a)?; + Self::Redeem(b) => { + e.u8(2)?; e.encode(b)?; Ok(()) @@ -146,7 +203,7 @@ impl minicbor::Encode for AddrAttrProperty { } } -pub type AddrAttr = OrderPreservingProperties; +pub type AddrAttrs = OrderPreservingProperties; #[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, PartialOrd)] pub struct AddressPayload { @@ -154,32 +211,96 @@ pub struct AddressPayload { pub root: AddressId, #[n(1)] - pub attributes: AddrAttr, + pub attributes: AddrAttrs, #[n(2)] pub addrtype: AddrType, } +use sha3::{Digest, Sha3_256}; +impl AddressPayload { + pub fn hash_address_id( + addrtype: &AddrType, + spending_data: &SpendingData, + attributes: &AddrAttrs, + ) -> Hash<28> { + let parts = (addrtype, spending_data, attributes); + let buf = minicbor::to_vec(parts).unwrap(); + + let mut sha = Sha3_256::new(); + sha.update(buf); + let sha = sha.finalize(); + + pallas_crypto::hash::Hasher::<224>::hash(&sha) + } + + pub fn new(addrtype: AddrType, spending_data: SpendingData, attributes: AddrAttrs) -> Self { + AddressPayload { + root: Self::hash_address_id(&addrtype, &spending_data, &attributes), + attributes, + addrtype, + } + } + + // bootstrap era + no hdpayload address + pub fn new_redeem( + pubkey: pallas_crypto::key::ed25519::PublicKey, + network_tag: Option>, + ) -> Self { + let spending_data = SpendingData::Redeem(ByteVec::from(Vec::from(pubkey.as_ref()))); + + let attributes = match network_tag { + Some(x) => vec![ + //AddrAttrProperty::DerivationPath(ByteVec::from(vec![])), + //AddrAttrProperty::AddrDistr(AddrDistr::BootstrapEraDistribution), + AddrAttrProperty::NetworkTag(x.into()), + ] + .into(), + None => vec![ + //AddrAttrProperty::DerivationPath(ByteVec::from(vec![])), + //AddrAttrProperty::AddrDistr(AddrDistr::BootstrapEraDistribution), + ] + .into(), + }; + + Self::new(AddrType::Redeem, spending_data, attributes) + } +} + +impl From for ByronAddress { + fn from(value: AddressPayload) -> Self { + ByronAddress::from_decoded(value) + } +} + /// New type wrapping a Byron address primitive #[derive(Debug, Encode, Decode, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct ByronAddress { #[n(0)] - payload: TagWrap, + pub payload: TagWrap, #[n(1)] - crc: u64, + pub crc: u32, } +const CRC: crc::Crc = crc::Crc::::new(&crc::CRC_32_ISO_HDLC); + impl ByronAddress { - pub fn new(payload: &[u8], crc: u64) -> Self { + pub fn new(payload: &[u8], crc: u32) -> Self { Self { payload: TagWrap(ByteVec::from(Vec::from(payload))), crc, } } + pub fn from_decoded(payload: AddressPayload) -> Self { + let payload = minicbor::to_vec(payload).unwrap(); + let c = CRC.checksum(&payload); + ByronAddress::new(&payload, c) + } + pub fn from_bytes(value: &[u8]) -> Result { - pallas_codec::minicbor::decode(value).map_err(|_| Error::InvalidByronCbor) + pallas_codec::minicbor::decode(value).map_err(Error::InvalidByronCbor) } // Tries to decode an address from its hex representation @@ -208,7 +329,7 @@ impl ByronAddress { } pub fn decode(&self) -> Result { - minicbor::decode(&self.payload.0).map_err(|_| Error::InvalidByronCbor) + minicbor::decode(&self.payload.0).map_err(Error::InvalidByronCbor) } } @@ -216,21 +337,38 @@ impl ByronAddress { mod tests { use super::*; - const TEST_VECTOR: &str = "37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na"; - - const ROOT_HASH: &str = "7e9ee4a9527dea9091e2d580edd6716888c42f75d96276290f98fe0b"; + const TEST_VECTORS: [&str; 3] = [ + "37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na", + "DdzFFzCqrht7PQiAhzrn6rNNoADJieTWBt8KeK9BZdUsGyX9ooYD9NpMCTGjQoUKcHN47g8JMXhvKogsGpQHtiQ65fZwiypjrC6d3a4Q", + "Ae2tdPwUPEZLs4HtbuNey7tK4hTKrwNwYtGqp7bDfCy2WdR3P6735W5Yfpe", + ]; #[test] fn roundtrip_base58() { - let addr = ByronAddress::from_base58(TEST_VECTOR).unwrap(); - let ours = addr.to_base58(); - assert_eq!(TEST_VECTOR, ours); + for vector in TEST_VECTORS { + let addr = ByronAddress::from_base58(vector).unwrap(); + let ours = addr.to_base58(); + assert_eq!(vector, ours); + } + } + + #[test] + fn roundtrip_cbor() { + for vector in TEST_VECTORS { + let addr = ByronAddress::from_base58(vector).unwrap(); + let addr = addr.decode().unwrap(); + let addr = ByronAddress::from_decoded(addr); + let ours = addr.to_base58(); + assert_eq!(vector, ours); + } } #[test] - fn payload_matches() { - let addr = ByronAddress::from_base58(TEST_VECTOR).unwrap(); - let payload = addr.decode().unwrap(); - assert_eq!(payload.root.to_string(), ROOT_HASH); + fn payload_crc_matches() { + for vector in TEST_VECTORS { + let addr = ByronAddress::from_base58(vector).unwrap(); + let crc2 = CRC.checksum(addr.payload.as_ref()); + assert_eq!(crc2, addr.crc); + } } } diff --git a/pallas-addresses/src/lib.rs b/pallas-addresses/src/lib.rs index ca3520b7..3660fc6d 100644 --- a/pallas-addresses/src/lib.rs +++ b/pallas-addresses/src/lib.rs @@ -41,8 +41,8 @@ pub enum Error { #[error("invalid operation for address content")] InvalidForContent, - #[error("invalid CBOR for Byron address")] - InvalidByronCbor, + #[error("invalid CBOR for Byron address {0}")] + InvalidByronCbor(pallas_codec::minicbor::decode::Error), #[error("unkown hrp for network {0:08b}")] UnknownNetworkHrp(u8), @@ -377,7 +377,7 @@ parse_shelley_fn!(parse_type_7, script_hash); // type 8 (1000) are Byron addresses fn parse_type_8(header: u8, payload: &[u8]) -> Result { let vec = [&[header], payload].concat(); - let inner = pallas_codec::minicbor::decode(&vec).map_err(|_| Error::InvalidByronCbor)?; + let inner = pallas_codec::minicbor::decode(&vec).map_err(Error::InvalidByronCbor)?; Ok(Address::Byron(inner)) } @@ -799,7 +799,7 @@ mod tests { let addr = Address::from_str(original).unwrap(); match addr { - Address::Byron(_) => assert!(matches!(addr.network(), None)), + Address::Byron(_) => assert!(addr.network().is_none()), _ => assert!(matches!(addr.network(), Some(Network::Mainnet))), } } diff --git a/pallas-applying/Cargo.toml b/pallas-applying/Cargo.toml new file mode 100644 index 00000000..a459f944 --- /dev/null +++ b/pallas-applying/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pallas-applying" +description = "Logic for validating and applying new blocks and txs to the chain state" +version = "0.19.1" +edition = "2021" +repository = "https://github.com/MaicoLeberle/pallas" +homepage = "https://github.com/MaicoLeberle/pallas" +license = "Apache-2.0" +readme = "README.md" +authors = ["Maico Leberle "] + +[lib] +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" diff --git a/pallas-applying/README.md b/pallas-applying/README.md new file mode 100644 index 00000000..e1d43db0 --- /dev/null +++ b/pallas-applying/README.md @@ -0,0 +1,3 @@ +# Pallas Applying + +Crate for performing transaction validation according to the Cardano protocol. diff --git a/pallas-applying/docs/byron-validation-rules.md b/pallas-applying/docs/byron-validation-rules.md new file mode 100644 index 00000000..7a7750ef --- /dev/null +++ b/pallas-applying/docs/byron-validation-rules.md @@ -0,0 +1,66 @@ +# Byron transaction validation rules + +Refer to the [Byron's ledger white paper](https://github.com/input-output-hk/cardano-ledger/releases/latest/download/byron-ledger.pdf) for further information. + +## Definitions and notation +- ***Tx*** is the set of Byron transactions, made of a ***TxBody*** (see below). + - ***txSize : Tx -> ℕ*** gives the size of the transaction. + - ***TxBody := P(TxIn) x P(TxOut)*** is the type of transaction bodies, each one composed of some inputs and some outputs. + - ***txBody : Tx → TxBody***. + - ***TxOut := Addr x Lovelace*** is the set of transaction outputs, where + - ***Addr*** is the set of transaction output addresses. + - ***Lovelace := ℕ***. + - ***txOuts : Tx → P(TxOut)*** gives the set of transaction outputs of a transaction. + - ***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). + - ***utxo : TxIn → TxOut*** gives the unspent transaction output (UTxO) associated with a transaction input. + - ***txIns : Tx → P(TxIn)*** gives the set of transaction inputs of a transaction. + - We write ***txIns(tx) ◁ utxo := {to ∈ TxOut / ∃ ti ∈ dom(utxo): utxo(ti) = to}*** to express the set of unspent transaction outputs associated with a set of transaction inputs. + - ***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). + - ***⟦_⟧A : 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. + - ***addrHashutxo : TxIn -> KeyHash*** takes a transaction input, extracts its associated transaction output from ***utxo***, extracts the address contained in it, and returns its hash. In other words, given ***utxo*** and transaction input ***i*** such that ***utxo(i) = (a, _)***, we have that ***addrHashutxo(i) := hash(a)***. +- **Protocol Parameters**: + - ***pps ∈ PParams*** is the set of (Byron) protocol parameters, with the following associated functions: + - ***minFees : PParams x Tx → ℕ*** gives the minimum amount of fees that must be paid for the transaction as determined by the protocol parameters. If ***tx*** spends only genesis UTxOs (i.e., only input UTxOs generated at the genesis of the ledger), then ***minFees(pps, tx) = 0***. + - ***maxTxSize : PParams → ℕ*** gives the (global) maximum transaction size. +- ***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, then ***sig(sk, d) = σ*** implies that ***verify(vk, σ, d) = true***. + - ***wits : Tx -> P(VKey x Sig)*** gives the list of pairs of verification keys and signatures of the transaction. + +## Validation rules +Byron phase-1 validation is successful on ***tx ∈ Tx*** if and only if + +- **The set of transaction inputs is not empty**: + + txIns(tx) ≠ ∅ +- **The set of transaction outputs is not empty**: + + txOuts(tx) ≠ ∅ +- **All transaction outputs contain non-null Lovelace values**: + + ∀ (_, c) ∈ txOuts(tx): 0 < c +- **All transaction inputs are in the set of (yet) unspent transaction outputs**: + + txIns(tx) ⊆ dom(utxo) +- **Fees are not less than what is determined by the protocol**: + + fees(tx) ≥ minFees(pps, tx) +- **The transaction size does not exceed the protocol limit**: + + txSize(tx) ≤ maxTxSize(pps) +- **The owner of each transaction input signed the transaction**: for each ***i ∈ txIns(tx)*** there exists ***(vk, σ) ∈ wits(tx)*** such that: + - verify(vk, σ, ⟦txBody(tx)⟧TxBody) + - addr_hashutxo(i) = hash(vk) diff --git a/pallas-applying/src/byron.rs b/pallas-applying/src/byron.rs new file mode 100644 index 00000000..0818da22 --- /dev/null +++ b/pallas-applying/src/byron.rs @@ -0,0 +1,275 @@ +//! Utilities required for Byron-era transaction validation. + +use std::borrow::Cow; + +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; + +pub fn validate_byron_tx( + 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; + let mut only_redeem_utxos: bool = true; + for input in tx.inputs.iter() { + if !is_redeem_utxo(input, utxos) { + only_redeem_utxos = false; + } + 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), + } + } + if only_redeem_utxos { + Ok(()) + } else { + 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 { + Err(ValidationError::FeesBelowMin) + } else { + Ok(()) + } + } +} + +fn is_redeem_utxo(input: &TxIn, utxos: &UTxOs) -> bool { + match find_tx_out(input, utxos) { + Ok(tx_out) => { + let address: ByronAddress = mk_byron_address(&tx_out.address); + match address.decode() { + Ok(addr_payload) => matches!(addr_payload.addrtype, AddrType::Redeem), + _ => false, + } + } + _ => false, + } +} + +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 { + let mut buff: Vec = 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 = &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 = 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, 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, ValidationError> { + let buff: &mut Vec = &mut Vec::new(); + let mut enc: Encoder<&mut Vec> = 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) +} diff --git a/pallas-applying/src/lib.rs b/pallas-applying/src/lib.rs new file mode 100644 index 00000000..78f48b3a --- /dev/null +++ b/pallas-applying/src/lib.rs @@ -0,0 +1,24 @@ +//! Logic for validating and applying new blocks and txs to the chain state + +pub mod byron; +pub mod types; + +use byron::validate_byron_tx; + +use pallas_traverse::{MultiEraTx, MultiEraTx::Byron as ByronTxPayload}; + +pub use types::{Environment, MultiEraProtParams, UTxOs, ValidationResult}; + +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(()), + } +} diff --git a/pallas-applying/src/types.rs b/pallas-applying/src/types.rs new file mode 100644 index 00000000..a6b97125 --- /dev/null +++ b/pallas-applying/src/types.rs @@ -0,0 +1,51 @@ +//! Base types used for validating transactions in each era. + +use std::collections::HashMap; + +pub use pallas_traverse::{MultiEraInput, MultiEraOutput}; + +pub type UTxOs<'b> = HashMap, MultiEraOutput<'b>>; + +#[derive(Debug, Clone)] +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 { + Byron(ByronProtParams), +} + +#[derive(Debug)] +pub struct Environment { + pub prot_params: MultiEraProtParams, + pub prot_magic: u32, +} + +#[non_exhaustive] +pub enum SigningTag { + Tx = 0x01, + RedeemTx = 0x02, +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum ValidationError { + InputMissingInUTxO, + TxInsEmpty, + TxOutsEmpty, + OutputWithoutLovelace, + UnknownTxSize, + UnableToComputeFees, + FeesBelowMin, + MaxTxSizeExceeded, + UnableToProcessWitnesses, + MissingWitness, + WrongSignature, +} + +pub type ValidationResult = Result<(), ValidationError>; diff --git a/pallas-applying/tests/README.md b/pallas-applying/tests/README.md new file mode 100644 index 00000000..2912f2ab --- /dev/null +++ b/pallas-applying/tests/README.md @@ -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. diff --git a/pallas-applying/tests/byron.rs b/pallas-applying/tests/byron.rs new file mode 100644 index 00000000..325f212b --- /dev/null +++ b/pallas-applying/tests/byron.rs @@ -0,0 +1,419 @@ +use std::{borrow::Cow, vec::Vec}; + +use pallas_applying::{ + types::{ByronProtParams, Environment, MultiEraProtParams, ValidationError}, + validate, UTxOs, ValidationResult, +}; +use pallas_codec::{ + minicbor::{ + bytes::ByteVec, + decode::{Decode, Decoder}, + encode, + }, + utils::{CborWrap, KeepRaw, MaybeIndefArray, TagWrap}, +}; +use pallas_primitives::byron::{Address, MintedTxPayload, Twit, Tx, TxIn, TxOut, Witnesses}; +use pallas_traverse::{MultiEraInput, MultiEraOutput, MultiEraTx}; + +#[cfg(test)] +mod byron_tests { + use super::*; + + fn cbor_to_bytes(input: &str) -> Vec { + hex::decode(input).unwrap() + } + + fn mainnet_tx_from_bytes_cbor<'a>(tx_cbor: &'a Vec) -> MintedTxPayload<'a> { + pallas_codec::minicbor::decode::(&tx_cbor[..]).unwrap() + } + + // Careful: this function assumes tx has exactly one input. + fn mk_utxo_for_single_input_tx<'a>(tx: &Tx, address_payload: String, amount: u64) -> UTxOs<'a> { + let mut tx_ins: Vec = tx.inputs.clone().to_vec(); + assert_eq!(tx_ins.len(), 1, "Unexpected number of inputs."); + let tx_in: TxIn = tx_ins.pop().unwrap(); + let input_tx_out_addr: Address = match hex::decode(address_payload) { + Ok(addr_bytes) => Address { + payload: TagWrap(ByteVec::from(addr_bytes)), + crc: 3430631884, + }, + _ => panic!("Unable to decode input address."), + }; + let tx_out: TxOut = TxOut { + address: input_tx_out_addr, + amount: amount, + }; + let mut utxos: UTxOs = new_utxos(); + add_to_utxo(&mut utxos, tx_in, tx_out); + utxos + } + + #[test] + fn successful_mainnet_tx_with_genesis_utxos() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron2.tx")); + let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron2.address")), + // The number of lovelace in this input is irrelevant, since no fees have to be paid + // for this transaction. + 1, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?}).", err), + } + } + + #[test] + fn successful_mainnet_tx() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => (), + Err(err) => assert!(false, "Unexpected error ({:?}).", err), + } + } + + #[test] + // Identical to successful_mainnet_tx, except that all inputs are removed. + fn empty_ins() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + // Clear the set of inputs in the transaction. + let mut tx: Tx = (*mtxp.transaction).clone(); + tx.inputs = MaybeIndefArray::Def(Vec::new()); + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Inputs set should not be empty."), + Err(err) => match err { + ValidationError::TxInsEmpty => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // Identical to successful_mainnet_tx, except that all outputs are removed. + fn empty_outs() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + // Clear the set of outputs in the transaction. + let mut tx: Tx = (*mtxp.transaction).clone(); + tx.outputs = MaybeIndefArray::Def(Vec::new()); + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Outputs set should not be empty."), + Err(err) => match err { + ValidationError::TxOutsEmpty => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // The transaction is valid, but the UTxO set is empty. + fn unfound_utxo() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = UTxOs::new(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must be within the UTxO set."), + Err(err) => match err { + ValidationError::InputMissingInUTxO => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // All lovelace in one of the outputs was removed. + fn output_without_lovelace() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + // Remove lovelace from output. + let mut tx: Tx = (*mtxp.transaction).clone(); + let altered_tx_out: TxOut = TxOut { + address: tx.outputs[0].address.clone(), + amount: 0, + }; + let mut new_tx_outs: Vec = Vec::new(); + new_tx_outs.push(tx.outputs[1].clone()); + new_tx_outs.push(altered_tx_out); + tx.outputs = MaybeIndefArray::Indef(new_tx_outs); + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.transaction = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "All outputs must contain lovelace."), + Err(err) => match err { + ValidationError::OutputWithoutLovelace => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // Expected fees are increased by increasing the protocol parameters. + fn not_enough_fees() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 1000, + min_fees_factor: 1000, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Fees should not be below minimum."), + Err(err) => match err { + ValidationError::FeesBelowMin => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // Tx size limit set by protocol parameters is established at 0. + fn tx_size_exceeds_max() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 0, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Transaction size cannot exceed protocol limit."), + Err(err) => match err { + ValidationError::MaxTxSizeExceeded => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // The input to the transaction does not have a corresponding witness. + fn missing_witness() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + // Remove witness + let new_witnesses: Witnesses = MaybeIndefArray::Def(Vec::new()); + let mut tx_buf: Vec = Vec::new(); + match encode(new_witnesses, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.witness = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "All inputs must have a witness signature."), + Err(err) => match err { + ValidationError::MissingWitness => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } + + #[test] + // The input to the transaction has an associated witness, but the signature is + // wrong. + fn wrong_signature() { + let cbor_bytes: Vec = cbor_to_bytes(include_str!("../../test_data/byron1.tx")); + let mut mtxp: MintedTxPayload = mainnet_tx_from_bytes_cbor(&cbor_bytes); + let utxos: UTxOs = mk_utxo_for_single_input_tx( + &mtxp.transaction, + String::from(include_str!("../../test_data/byron1.address")), + 19999000000, + ); + // Modify signature in witness + let new_wit: Twit = match mtxp.witness[0].clone() { + Twit::PkWitness(CborWrap((pk, _))) => { + Twit::PkWitness(CborWrap((pk, [0u8; 64].to_vec().into()))) + } + _ => unreachable!(), + }; + let mut new_witnesses_vec = Vec::new(); + new_witnesses_vec.push(new_wit); + let new_witnesses: Witnesses = MaybeIndefArray::Def(new_witnesses_vec); + let mut tx_buf: Vec = Vec::new(); + match encode(new_witnesses, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + mtxp.witness = Decode::decode(&mut Decoder::new(&tx_buf.as_slice()), &mut ()).unwrap(); + let env: Environment = Environment { + prot_params: MultiEraProtParams::Byron(ByronProtParams { + min_fees_const: 155381, + min_fees_factor: 44, + max_tx_size: 4096, + }), + prot_magic: 764824073, + }; + match mk_byron_tx_and_validate(&mtxp.transaction, &mtxp.witness, &utxos, &env) { + Ok(()) => assert!(false, "Witness signature should verify the transaction."), + Err(err) => match err { + ValidationError::WrongSignature => (), + _ => assert!(false, "Unexpected error ({:?}).", err), + }, + } + } +} + +// Helper functions. +fn add_to_utxo<'a>(utxos: &mut UTxOs<'a>, tx_in: TxIn, tx_out: TxOut) { + let multi_era_in: MultiEraInput = MultiEraInput::Byron(Box::new(Cow::Owned(tx_in))); + let multi_era_out: MultiEraOutput = MultiEraOutput::Byron(Box::new(Cow::Owned(tx_out))); + utxos.insert(multi_era_in, multi_era_out); +} + +// pallas_applying::validate takes a MultiEraTx, not a Tx and a Witnesses. To be +// able to build a MultiEraTx from a Tx and a Witnesses, we need to encode each +// of them and then decode them into KeepRaw and KeepRaw values, +// respectively, to be able to make the MultiEraTx value. +fn mk_byron_tx_and_validate( + tx: &Tx, + wits: &Witnesses, + utxos: &UTxOs, + env: &Environment, +) -> ValidationResult { + let mut tx_buf: Vec = Vec::new(); + match encode(tx, &mut tx_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Tx ({:?}).", err), + }; + let kptx: KeepRaw = match Decode::decode(&mut Decoder::new(tx_buf.as_slice()), &mut ()) { + Ok(kp) => kp, + Err(err) => panic!("Unable to decode Tx ({:?}).", err), + }; + + let mut wit_buf: Vec = Vec::new(); + match encode(wits, &mut wit_buf) { + Ok(_) => (), + Err(err) => assert!(false, "Unable to encode Witnesses ({:?}).", err), + }; + let kpwit: KeepRaw = + match Decode::decode(&mut Decoder::new(wit_buf.as_slice()), &mut ()) { + Ok(kp) => kp, + Err(err) => panic!("Unable to decode Witnesses ({:?}).", err), + }; + + let mtxp: MintedTxPayload = MintedTxPayload { + transaction: kptx, + witness: kpwit, + }; + let metx: MultiEraTx = MultiEraTx::from_byron(&mtxp); + validate(&metx, utxos, env) +} + +fn new_utxos<'a>() -> UTxOs<'a> { + UTxOs::new() +} diff --git a/pallas-codec/Cargo.toml b/pallas-codec/Cargo.toml index 50e9d2c9..0bccfa97 100644 --- a/pallas-codec/Cargo.toml +++ b/pallas-codec/Cargo.toml @@ -1,16 +1,24 @@ [package] name = "pallas-codec" description = "Pallas common CBOR encoding interface and utilities" -version = "0.19.0-alpha.1" +version = "0.19.1" edition = "2021" repository = "https://github.com/txpipe/pallas" homepage = "https://github.com/txpipe/pallas" documentation = "https://docs.rs/pallas-codec" license = "Apache-2.0" readme = "README.md" -authors = ["Santiago Carmuega "] +authors = [ + "Santiago Carmuega ", + "Lucas Rosa ", + "Kasey White ", +] [dependencies] hex = "0.4.3" minicbor = { version = "0.19", features = ["std", "half", "derive"] } serde = { version = "1.0.143", features = ["derive"] } +thiserror = "1.0.39" + +[dev-dependencies] +proptest = "1.1.0" diff --git a/pallas-codec/README.md b/pallas-codec/README.md index 6f6cb36d..4e83364c 100644 --- a/pallas-codec/README.md +++ b/pallas-codec/README.md @@ -1,2 +1,5 @@ # Pallas Codec +## Flat + +A Rust port of the [Haskell reference implementation](https://github.com/Quid2/flat). diff --git a/pallas-codec/src/flat/decode/decoder.rs b/pallas-codec/src/flat/decode/decoder.rs new file mode 100644 index 00000000..63dc8fd5 --- /dev/null +++ b/pallas-codec/src/flat/decode/decoder.rs @@ -0,0 +1,352 @@ +use super::Decode; +use crate::flat::zigzag; + +use super::Error; + +#[derive(Debug)] +pub struct Decoder<'b> { + pub buffer: &'b [u8], + pub used_bits: i64, + pub pos: usize, +} + +impl<'b> Decoder<'b> { + pub fn new(bytes: &'b [u8]) -> Decoder { + Decoder { + buffer: bytes, + pos: 0, + used_bits: 0, + } + } + + /// Decode any type that implements [`Decode`]. + pub fn decode>(&mut self) -> Result { + T::decode(self) + } + + /// Decode an integer of any size. + /// This is byte alignment agnostic. + /// First we decode the next 8 bits of the buffer. + /// We take the 7 least significant bits as the 7 least significant bits of + /// the current unsigned integer. If the most significant bit of the 8 + /// bits is 1 then we take the next 8 and repeat the process above, + /// filling in the next 7 least significant bits of the unsigned integer and + /// so on. If the most significant bit was instead 0 we stop decoding + /// any more bits. Finally we use zigzag to convert the unsigned integer + /// back to a signed integer. + pub fn integer(&mut self) -> Result { + Ok(zigzag::to_isize(self.word()?)) + } + + /// Decode an integer of 128 bits size. + /// This is byte alignment agnostic. + /// First we decode the next 8 bits of the buffer. + /// We take the 7 least significant bits as the 7 least significant bits of + /// the current unsigned integer. If the most significant bit of the 8 + /// bits is 1 then we take the next 8 and repeat the process above, + /// filling in the next 7 least significant bits of the unsigned integer and + /// so on. If the most significant bit was instead 0 we stop decoding + /// any more bits. Finally we use zigzag to convert the unsigned integer + /// back to a signed integer. + pub fn big_integer(&mut self) -> Result { + Ok(zigzag::to_i128(self.big_word()?)) + } + + /// Decode a single bit of the buffer to get a bool. + /// We mask out a single bit of the buffer based on used bits. + /// and check if it is 0 for false or 1 for true. + // TODO: use bit() instead of this custom implementation. + pub fn bool(&mut self) -> Result { + let current_byte = self.buffer[self.pos]; + let b = 0 != (current_byte & (128 >> self.used_bits)); + self.increment_buffer_by_bit(); + Ok(b) + } + + /// Decode a byte from the buffer. + /// This byte alignment agnostic. + /// We use the next 8 bits in the buffer and return the resulting byte. + pub fn u8(&mut self) -> Result { + self.bits8(8) + } + + /// Decode a byte array. + /// Decodes a filler to byte align the buffer, + /// then decodes the next byte to get the array length up to a max of 255. + /// We decode bytes equal to the array length to form the byte array. + /// If the following byte for array length is not 0 we decode it and repeat + /// above to continue decoding the byte array. We stop once we hit a + /// byte array length of 0. If array length is 0 for first byte array + /// length the we return a empty array. + pub fn bytes(&mut self) -> Result, Error> { + self.filler()?; + self.byte_array() + } + + /// Decode a 32 bit char. + /// This is byte alignment agnostic. + /// First we decode the next 8 bits of the buffer. + /// We take the 7 least significant bits as the 7 least significant bits of + /// the current unsigned integer. If the most significant bit of the 8 + /// bits is 1 then we take the next 8 and repeat the process above, + /// filling in the next 7 least significant bits of the unsigned integer and + /// so on. If the most significant bit was instead 0 we stop decoding + /// any more bits. + pub fn char(&mut self) -> Result { + let character = self.word()? as u32; + + char::from_u32(character).ok_or(Error::DecodeChar(character)) + } + + // TODO: Do we need this? + pub fn string(&mut self) -> Result { + let mut s = String::new(); + while self.bit()? { + s += &self.char()?.to_string(); + } + Ok(s) + } + + /// Decode a string. + /// Convert to byte array and then use byte array decoding. + /// Decodes a filler to byte align the buffer, + /// then decodes the next byte to get the array length up to a max of 255. + /// We decode bytes equal to the array length to form the byte array. + /// If the following byte for array length is not 0 we decode it and repeat + /// above to continue decoding the byte array. We stop once we hit a + /// byte array length of 0. If array length is 0 for first byte array + /// length the we return a empty array. + pub fn utf8(&mut self) -> Result { + // TODO: Better Error Handling + String::from_utf8(Vec::::decode(self)?).map_err(Error::from) + } + + /// Decodes a filler of max one byte size. + /// Decodes bits until we hit a bit that is 1. + /// Expects that the 1 is at the end of the current byte in the buffer. + pub fn filler(&mut self) -> Result<(), Error> { + while self.zero()? {} + Ok(()) + } + + /// Decode a word of any size. + /// This is byte alignment agnostic. + /// First we decode the next 8 bits of the buffer. + /// We take the 7 least significant bits as the 7 least significant bits of + /// the current unsigned integer. If the most significant bit of the 8 + /// bits is 1 then we take the next 8 and repeat the process above, + /// filling in the next 7 least significant bits of the unsigned integer and + /// so on. If the most significant bit was instead 0 we stop decoding + /// any more bits. + pub fn word(&mut self) -> Result { + let mut leading_bit = 1; + let mut final_word: usize = 0; + let mut shl: usize = 0; + // continue looping if lead bit is 1 which is 128 as a u8 otherwise exit + while leading_bit > 0 { + let word8 = self.bits8(8)?; + let word7 = word8 & 127; + final_word |= (word7 as usize) << shl; + shl += 7; + leading_bit = word8 & 128; + } + Ok(final_word) + } + + /// Decode a word of 128 bits size. + /// This is byte alignment agnostic. + /// First we decode the next 8 bits of the buffer. + /// We take the 7 least significant bits as the 7 least significant bits of + /// the current unsigned integer. If the most significant bit of the 8 + /// bits is 1 then we take the next 8 and repeat the process above, + /// filling in the next 7 least significant bits of the unsigned integer and + /// so on. If the most significant bit was instead 0 we stop decoding + /// any more bits. + pub fn big_word(&mut self) -> Result { + let mut leading_bit = 1; + let mut final_word: u128 = 0; + let mut shl: u128 = 0; + // continue looping if lead bit is 1 which is 128 as a u8 otherwise exit + while leading_bit > 0 { + let word8 = self.bits8(8)?; + let word7 = word8 & 127; + final_word |= (word7 as u128) << shl; + shl += 7; + leading_bit = word8 & 128; + } + Ok(final_word) + } + + /// Decode a list of items with a decoder function. + /// This is byte alignment agnostic. + /// Decode a bit from the buffer. + /// If 0 then stop. + /// Otherwise we decode an item in the list with the decoder function passed + /// in. Then decode the next bit in the buffer and repeat above. + /// Returns a list of items decoded with the decoder function. + pub fn decode_list_with(&mut self, decoder_func: F) -> Result, Error> + where + F: Copy + FnOnce(&mut Decoder) -> Result, + { + let mut vec_array: Vec = Vec::new(); + while self.bit()? { + vec_array.push(decoder_func(self)?) + } + Ok(vec_array) + } + + pub fn decode_list_with_debug( + &mut self, + decoder_func: F, + state_log: &mut Vec, + ) -> Result, Error> + where + F: Copy + FnOnce(&mut Decoder, &mut Vec) -> Result, + { + let mut vec_array: Vec = Vec::new(); + while self.bit()? { + vec_array.push(decoder_func(self, state_log)?) + } + Ok(vec_array) + } + + /// Decode the next bit in the buffer. + /// If the bit was 0 then return true. + /// Otherwise return false. + /// Throws EndOfBuffer error if used at the end of the array. + fn zero(&mut self) -> Result { + let current_bit = self.bit()?; + + Ok(!current_bit) + } + + /// Decode the next bit in the buffer. + /// If the bit was 1 then return true. + /// Otherwise return false. + /// Throws EndOfBuffer error if used at the end of the array. + fn bit(&mut self) -> Result { + if self.pos >= self.buffer.len() { + return Err(Error::EndOfBuffer); + } + + let b = self.buffer[self.pos] & (128 >> self.used_bits) > 0; + + self.increment_buffer_by_bit(); + + Ok(b) + } + + /// Decode a byte array. + /// Throws a BufferNotByteAligned error if the buffer is not byte aligned + /// Decodes the next byte to get the array length up to a max of 255. + /// We decode bytes equal to the array length to form the byte array. + /// If the following byte for array length is not 0 we decode it and repeat + /// above to continue decoding the byte array. We stop once we hit a + /// byte array length of 0. If array length is 0 for first byte array + /// length the we return a empty array. + fn byte_array(&mut self) -> Result, Error> { + if self.used_bits != 0 { + return Err(Error::BufferNotByteAligned); + } + + self.ensure_bytes(1)?; + + let mut blk_len = self.buffer[self.pos]; + + self.pos += 1; + + let mut blk_array: Vec = Vec::new(); + + while blk_len != 0 { + self.ensure_bytes(blk_len as usize + 1)?; + + let decoded_array = &self.buffer[self.pos..self.pos + blk_len as usize]; + + blk_array.extend(decoded_array); + + self.pos += blk_len as usize; + + blk_len = self.buffer[self.pos]; + + self.pos += 1 + } + + Ok(blk_array) + } + + /// Decode up to 8 bits. + /// This is byte alignment agnostic. + /// If num_bits is greater than the 8 we throw an IncorrectNumBits error. + /// First we decode the next num_bits of bits in the buffer. + /// If there are less unused bits in the current byte in the buffer than + /// num_bits, then we decode the remaining bits from the most + /// significant bits in the next byte in the buffer. Otherwise we decode + /// the unused bits from the current byte. Returns the decoded value up + /// to a byte in size. + pub fn bits8(&mut self, num_bits: usize) -> Result { + if num_bits > 8 { + return Err(Error::IncorrectNumBits); + } + + self.ensure_bits(num_bits)?; + + let unused_bits = 8 - self.used_bits as usize; + let leading_zeroes = 8 - num_bits; + let r = (self.buffer[self.pos] << self.used_bits as usize) >> leading_zeroes; + + let x = if num_bits > unused_bits { + r | (self.buffer[self.pos + 1] >> (unused_bits + leading_zeroes)) + } else { + r + }; + + self.drop_bits(num_bits); + + Ok(x) + } + + /// Ensures the buffer has the required bytes passed in by required_bytes. + /// Throws a NotEnoughBytes error if there are less bytes remaining in the + /// buffer than required_bytes. + fn ensure_bytes(&mut self, required_bytes: usize) -> Result<(), Error> { + if required_bytes as isize > self.buffer.len() as isize - self.pos as isize { + Err(Error::NotEnoughBytes(required_bytes)) + } else { + Ok(()) + } + } + + /// Ensures the buffer has the required bits passed in by required_bits. + /// Throws a NotEnoughBits error if there are less bits remaining in the + /// buffer than required_bits. + fn ensure_bits(&mut self, required_bits: usize) -> Result<(), Error> { + if required_bits as isize + > (self.buffer.len() as isize - self.pos as isize) * 8 - self.used_bits as isize + { + Err(Error::NotEnoughBits(required_bits)) + } else { + Ok(()) + } + } + + /// Increment buffer by num_bits. + /// If num_bits + used bits is greater than 8, + /// then increment position by (num_bits + used bits) / 8 + /// Use the left over remainder as the new amount of used bits. + fn drop_bits(&mut self, num_bits: usize) { + let all_used_bits = num_bits as i64 + self.used_bits; + self.used_bits = all_used_bits % 8; + self.pos += all_used_bits as usize / 8; + } + + /// Increment used bits by 1. + /// If all 8 bits are used then increment buffer position by 1. + fn increment_buffer_by_bit(&mut self) { + if self.used_bits == 7 { + self.pos += 1; + self.used_bits = 0; + } else { + self.used_bits += 1; + } + } +} diff --git a/pallas-codec/src/flat/decode/error.rs b/pallas-codec/src/flat/decode/error.rs new file mode 100644 index 00000000..59b8c1fa --- /dev/null +++ b/pallas-codec/src/flat/decode/error.rs @@ -0,0 +1,23 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Reached end of buffer")] + EndOfBuffer, + #[error("Buffer is not byte aligned")] + BufferNotByteAligned, + #[error("Incorrect value of num_bits, must be less than 9")] + IncorrectNumBits, + #[error("Not enough data available, required {0} bytes")] + NotEnoughBytes(usize), + #[error("Not enough data available, required {0} bits")] + NotEnoughBits(usize), + #[error(transparent)] + DecodeUtf8(#[from] std::string::FromUtf8Error), + #[error("Decoding u32 to char {0}")] + DecodeChar(u32), + #[error("{0}")] + Message(String), + #[error("Unknown term constructor tag: {0}.\n\nHere are the buffer bytes ({1} preceding) {2}\n\nBuffer position is {3} and buffer length is {4}")] + UnknownTermConstructor(u8, usize, String, usize, usize), +} diff --git a/pallas-codec/src/flat/decode/mod.rs b/pallas-codec/src/flat/decode/mod.rs new file mode 100644 index 00000000..431c88a1 --- /dev/null +++ b/pallas-codec/src/flat/decode/mod.rs @@ -0,0 +1,67 @@ +mod decoder; +mod error; + +use crate::flat::filler::Filler; + +pub use decoder::Decoder; +pub use error::Error; + +pub trait Decode<'b>: Sized { + fn decode(d: &mut Decoder) -> Result; +} + +impl Decode<'_> for Filler { + fn decode(d: &mut Decoder) -> Result { + d.filler()?; + + Ok(Filler::FillerEnd) + } +} + +impl Decode<'_> for Vec { + fn decode(d: &mut Decoder) -> Result { + d.bytes() + } +} + +impl Decode<'_> for u8 { + fn decode(d: &mut Decoder) -> Result { + d.u8() + } +} + +impl Decode<'_> for isize { + fn decode(d: &mut Decoder) -> Result { + d.integer() + } +} + +impl Decode<'_> for i128 { + fn decode(d: &mut Decoder) -> Result { + d.big_integer() + } +} + +impl Decode<'_> for usize { + fn decode(d: &mut Decoder) -> Result { + d.word() + } +} + +impl Decode<'_> for char { + fn decode(d: &mut Decoder) -> Result { + d.char() + } +} + +impl Decode<'_> for String { + fn decode(d: &mut Decoder) -> Result { + d.utf8() + } +} + +impl Decode<'_> for bool { + fn decode(d: &mut Decoder) -> Result { + d.bool() + } +} diff --git a/pallas-codec/src/flat/encode/encoder.rs b/pallas-codec/src/flat/encode/encoder.rs new file mode 100644 index 00000000..d17525b7 --- /dev/null +++ b/pallas-codec/src/flat/encode/encoder.rs @@ -0,0 +1,338 @@ +use super::Encode; +use crate::flat::zigzag; + +use super::Error; + +pub struct Encoder { + pub buffer: Vec, + // Int + used_bits: i64, + // Int + current_byte: u8, +} + +impl Default for Encoder { + fn default() -> Self { + Self::new() + } +} + +impl Encoder { + pub fn new() -> Encoder { + Encoder { + buffer: Vec::new(), + used_bits: 0, + current_byte: 0, + } + } + + /// Encode any type that implements [`Encode`]. + pub fn encode(&mut self, x: T) -> Result<&mut Self, Error> { + x.encode(self)?; + + Ok(self) + } + + /// Encode 1 unsigned byte. + /// Uses the next 8 bits in the buffer, can be byte aligned or byte + /// unaligned + pub fn u8(&mut self, x: u8) -> Result<&mut Self, Error> { + if self.used_bits == 0 { + self.current_byte = x; + self.next_word(); + } else { + self.byte_unaligned(x); + } + + Ok(self) + } + + /// Encode a `bool` value. This is byte alignment agnostic. + /// Uses the next unused bit in the current byte to encode this information. + /// One for true and Zero for false + pub fn bool(&mut self, x: bool) -> &mut Self { + if x { + self.one(); + } else { + self.zero(); + } + + self + } + + /// Encode a byte array. + /// Uses filler to byte align the buffer, then writes byte array length up + /// to 255. Following that it writes the next 255 bytes from the array. + /// We repeat writing length up to 255 and the next 255 bytes until we reach + /// the end of the byte array. After reaching the end of the byte array + /// we write a 0 byte. Only write 0 byte if the byte array is empty. + pub fn bytes(&mut self, x: &[u8]) -> Result<&mut Self, Error> { + // use filler to write current buffer so bits used gets reset + self.filler(); + + self.byte_array(x) + } + + /// Encode a byte array in a byte aligned buffer. Throws exception if any + /// bits for the current byte were used. Writes byte array length up to + /// 255 Following that it writes the next 255 bytes from the array. + /// We repeat writing length up to 255 and the next 255 bytes until we reach + /// the end of the byte array. After reaching the end of the buffer we + /// write a 0 byte. Only write 0 if the byte array is empty. + pub fn byte_array(&mut self, arr: &[u8]) -> Result<&mut Self, Error> { + if self.used_bits != 0 { + return Err(Error::BufferNotByteAligned); + } + + self.write_blk(arr); + + Ok(self) + } + + /// Encode an integer of any size. + /// This is byte alignment agnostic. + /// First we use zigzag once to double the number and encode the negative + /// sign as the least significant bit. Next we encode the 7 least + /// significant bits of the unsigned integer. If the number is greater than + /// 127 we encode a leading 1 followed by repeating the encoding above for + /// the next 7 bits and so on. + pub fn integer(&mut self, i: isize) -> &mut Self { + let i = zigzag::to_usize(i); + + self.word(i); + + self + } + + /// Encode an integer of 128 bits size. + /// This is byte alignment agnostic. + /// First we use zigzag once to double the number and encode the negative + /// sign as the least significant bit. Next we encode the 7 least + /// significant bits of the unsigned integer. If the number is greater than + /// 127 we encode a leading 1 followed by repeating the encoding above for + /// the next 7 bits and so on. + pub fn big_integer(&mut self, i: i128) -> &mut Self { + let i = zigzag::to_u128(i); + + self.big_word(i); + + self + } + + /// Encode a char of 32 bits. + /// This is byte alignment agnostic. + /// We encode the 7 least significant bits of the unsigned byte. If the char + /// value is greater than 127 we encode a leading 1 followed by + /// repeating the above for the next 7 bits and so on. + pub fn char(&mut self, c: char) -> &mut Self { + self.word(c as usize); + + self + } + + // TODO: Do we need this? + pub fn string(&mut self, s: &str) -> &mut Self { + for i in s.chars() { + self.one(); + self.char(i); + } + + self.zero(); + + self + } + + /// Encode a string. + /// Convert to byte array and then use byte array encoding. + /// Uses filler to byte align the buffer, then writes byte array length up + /// to 255. Following that it writes the next 255 bytes from the array. + /// After reaching the end of the buffer we write a 0 byte. Only write 0 + /// byte if the byte array is empty. + pub fn utf8(&mut self, s: &str) -> Result<&mut Self, Error> { + self.bytes(s.as_bytes()) + } + + /// Encode a unsigned integer of any size. + /// This is byte alignment agnostic. + /// We encode the 7 least significant bits of the unsigned byte. If the char + /// value is greater than 127 we encode a leading 1 followed by + /// repeating the above for the next 7 bits and so on. + pub fn word(&mut self, c: usize) -> &mut Self { + let mut d = c; + loop { + let mut w = (d & 127) as u8; + d >>= 7; + + if d != 0 { + w |= 128; + } + self.bits(8, w); + + if d == 0 { + break; + } + } + + self + } + + /// Encode a unsigned integer of 128 bits size. + /// This is byte alignment agnostic. + /// We encode the 7 least significant bits of the unsigned byte. If the char + /// value is greater than 127 we encode a leading 1 followed by + /// repeating the above for the next 7 bits and so on. + pub fn big_word(&mut self, c: u128) -> &mut Self { + let mut d = c; + loop { + let mut w = (d & 127) as u8; + d >>= 7; + + if d != 0 { + w |= 128; + } + self.bits(8, w); + + if d == 0 { + break; + } + } + + self + } + + /// Encode a list of bytes with a function + /// This is byte alignment agnostic. + /// If there are bytes in a list then write 1 bit followed by the functions + /// encoding. After the last item write a 0 bit. If the list is empty + /// only encode a 0 bit. + pub fn encode_list_with( + &mut self, + list: &[T], + encoder_func: for<'r> fn(&T, &'r mut Encoder) -> Result<(), Error>, + ) -> Result<&mut Self, Error> { + for item in list { + self.one(); + encoder_func(item, self)?; + } + + self.zero(); + + Ok(self) + } + + /// Encodes up to 8 bits of information and is byte alignment agnostic. + /// Uses unused bits in the current byte to write out the passed in byte + /// value. Overflows to the most significant digits of the next byte if + /// number of bits to use is greater than unused bits. Expects that + /// number of bits to use is greater than or equal to required bits by the + /// value. The param num_bits is i64 to match unused_bits type. + pub fn bits(&mut self, num_bits: i64, val: u8) -> &mut Self { + match (num_bits, val) { + (1, 0) => self.zero(), + (1, 1) => self.one(), + (2, 0) => { + self.zero(); + self.zero(); + } + (2, 1) => { + self.zero(); + self.one(); + } + (2, 2) => { + self.one(); + self.zero(); + } + (2, 3) => { + self.one(); + self.one(); + } + (_, _) => { + self.used_bits += num_bits; + let unused_bits = 8 - self.used_bits; + match unused_bits { + 0 => { + self.current_byte |= val; + self.next_word(); + } + x if x > 0 => { + self.current_byte |= val << x; + } + x => { + let used = -x; + self.current_byte |= val >> used; + self.next_word(); + self.current_byte = val << (8 - used); + self.used_bits = used; + } + } + } + } + + self + } + + /// A filler amount of end 0's followed by a 1 at the end of a byte. + /// Used to byte align the buffer by padding out the rest of the byte. + pub(crate) fn filler(&mut self) -> &mut Self { + self.current_byte |= 1; + self.next_word(); + + self + } + + /// Write a 0 bit into the current byte. + /// Write out to buffer if last used bit in the current byte. + fn zero(&mut self) { + if self.used_bits == 7 { + self.next_word(); + } else { + self.used_bits += 1; + } + } + + /// Write a 1 bit into the current byte. + /// Write out to buffer if last used bit in the current byte. + fn one(&mut self) { + if self.used_bits == 7 { + self.current_byte |= 1; + self.next_word(); + } else { + self.current_byte |= 128 >> self.used_bits; + self.used_bits += 1; + } + } + /// Write out byte regardless of current buffer alignment. + /// Write most significant bits in remaining unused bits for the current + /// byte, then write out the remaining bits at the beginning of the next + /// byte. + fn byte_unaligned(&mut self, x: u8) { + let x_shift = self.current_byte | (x >> self.used_bits); + self.buffer.push(x_shift); + + self.current_byte = x << (8 - self.used_bits); + } + + /// Write the current byte out to the buffer and begin next byte to write + /// out. Add current byte to the buffer and set current byte and used + /// bits to 0. + fn next_word(&mut self) { + self.buffer.push(self.current_byte); + + self.current_byte = 0; + self.used_bits = 0; + } + + /// Writes byte array length up to 255 + /// Following that it writes the next 255 bytes from the array. + /// After reaching the end of the buffer we write a 0 byte. Only write 0 if + /// the byte array is empty. This is byte alignment agnostic. + fn write_blk(&mut self, arr: &[u8]) { + let chunks = arr.chunks(255); + + for chunk in chunks { + self.buffer.push(chunk.len() as u8); + self.buffer.extend(chunk); + } + self.buffer.push(0_u8); + } +} diff --git a/pallas-codec/src/flat/encode/error.rs b/pallas-codec/src/flat/encode/error.rs new file mode 100644 index 00000000..97b2cd8f --- /dev/null +++ b/pallas-codec/src/flat/encode/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Buffer is not byte aligned")] + BufferNotByteAligned, + #[error("{0}")] + Message(String), +} diff --git a/pallas-codec/src/flat/encode/mod.rs b/pallas-codec/src/flat/encode/mod.rs new file mode 100644 index 00000000..8b30bc91 --- /dev/null +++ b/pallas-codec/src/flat/encode/mod.rs @@ -0,0 +1,107 @@ +mod encoder; +mod error; + +use crate::flat::filler::Filler; + +pub use encoder::Encoder; +pub use error::Error; + +pub trait Encode { + fn encode(&self, e: &mut Encoder) -> Result<(), Error>; +} + +impl Encode for bool { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.bool(*self); + + Ok(()) + } +} + +impl Encode for u8 { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.u8(*self)?; + + Ok(()) + } +} + +impl Encode for i128 { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.big_integer(*self); + + Ok(()) + } +} + +impl Encode for isize { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.integer(*self); + + Ok(()) + } +} + +impl Encode for usize { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.word(*self); + + Ok(()) + } +} + +impl Encode for char { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.char(*self); + + Ok(()) + } +} + +impl Encode for &str { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.utf8(self)?; + + Ok(()) + } +} + +impl Encode for String { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.utf8(self)?; + + Ok(()) + } +} + +impl Encode for Vec { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.bytes(self)?; + + Ok(()) + } +} + +impl Encode for &[u8] { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.bytes(self)?; + + Ok(()) + } +} + +impl Encode for Box { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + self.as_ref().encode(e)?; + + Ok(()) + } +} + +impl Encode for Filler { + fn encode(&self, e: &mut Encoder) -> Result<(), Error> { + e.filler(); + + Ok(()) + } +} diff --git a/pallas-codec/src/flat/filler.rs b/pallas-codec/src/flat/filler.rs new file mode 100644 index 00000000..0efe8db9 --- /dev/null +++ b/pallas-codec/src/flat/filler.rs @@ -0,0 +1,13 @@ +pub enum Filler { + FillerStart(Box), + FillerEnd, +} + +impl Filler { + pub fn length(&self) -> usize { + match self { + Filler::FillerStart(f) => f.length() + 1, + Filler::FillerEnd => 1, + } + } +} diff --git a/pallas-codec/src/flat/mod.rs b/pallas-codec/src/flat/mod.rs new file mode 100644 index 00000000..dbaecdfc --- /dev/null +++ b/pallas-codec/src/flat/mod.rs @@ -0,0 +1,47 @@ +mod decode; +mod encode; +pub mod filler; +pub mod zigzag; + +pub mod en { + pub use super::encode::*; +} + +pub mod de { + pub use super::decode::*; +} + +pub trait Flat<'b>: en::Encode + de::Decode<'b> { + fn flat(&self) -> Result, en::Error> { + encode(self) + } + + fn unflat(bytes: &'b [u8]) -> Result { + decode(bytes) + } +} + +pub fn encode(value: &T) -> Result, en::Error> +where + T: en::Encode, +{ + let mut e = en::Encoder::new(); + + value.encode(&mut e)?; + e.encode(filler::Filler::FillerEnd)?; + + Ok(e.buffer) +} + +pub fn decode<'b, T>(bytes: &'b [u8]) -> Result +where + T: de::Decode<'b>, +{ + let mut d = de::Decoder::new(bytes); + + let value = d.decode()?; + + d.decode::()?; + + Ok(value) +} diff --git a/pallas-codec/src/flat/zigzag.rs b/pallas-codec/src/flat/zigzag.rs new file mode 100644 index 00000000..4b9f4c4a --- /dev/null +++ b/pallas-codec/src/flat/zigzag.rs @@ -0,0 +1,27 @@ +pub fn to_usize(x: isize) -> usize { + let double_x = x << 1; + + if x.is_positive() || x == 0 { + double_x as usize + } else { + (-double_x - 1) as usize + } +} + +pub fn to_isize(u: usize) -> isize { + ((u >> 1) as isize) ^ (-((u & 1) as isize)) +} + +pub fn to_u128(x: i128) -> u128 { + let double_x = x << 1; + + if x.is_positive() || x == 0 { + double_x as u128 + } else { + (-double_x - 1) as u128 + } +} + +pub fn to_i128(u: u128) -> i128 { + ((u >> 1) as i128) ^ (-((u & 1) as i128)) +} diff --git a/pallas-codec/src/lib.rs b/pallas-codec/src/lib.rs index 11787c4d..41776b24 100644 --- a/pallas-codec/src/lib.rs +++ b/pallas-codec/src/lib.rs @@ -1,3 +1,6 @@ +/// Flat encoding/decoding for Plutus Core +pub mod flat; + /// Shared re-export of minicbor lib across all Pallas pub use minicbor; diff --git a/pallas-codec/src/utils.rs b/pallas-codec/src/utils.rs index dca4f71a..c2db52e1 100644 --- a/pallas-codec/src/utils.rs +++ b/pallas-codec/src/utils.rs @@ -1,6 +1,6 @@ use minicbor::{data::Tag, Decode, Encode}; use serde::{Deserialize, Serialize}; -use std::{fmt, ops::Deref}; +use std::{fmt, hash::Hash as StdHash, ops::Deref}; /// Utility for skipping parts of the CBOR payload, use only for debugging #[derive(Debug, PartialEq, PartialOrd, Eq, Ord)] @@ -254,6 +254,12 @@ impl

Deref for OrderPreservingProperties

{ } } +impl

From> for OrderPreservingProperties

{ + fn from(value: Vec

) -> Self { + OrderPreservingProperties(value) + } +} + impl<'b, C, P> minicbor::decode::Decode<'b, C> for OrderPreservingProperties

where P: Decode<'b, C>, @@ -286,7 +292,7 @@ where } /// Wraps a struct so that it is encoded/decoded as a cbor bytes -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, StdHash)] #[serde(transparent)] pub struct CborWrap(pub T); @@ -384,7 +390,7 @@ impl Deref for TagWrap { /// An empty map /// /// don't ask me why, that's what the CDDL asks for. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct EmptyMap; impl<'b, C> minicbor::decode::Decode<'b, C> for EmptyMap { @@ -643,6 +649,86 @@ impl minicbor::Encode for KeepRaw<'_, T> { } } +/// Struct to hold arbitrary CBOR to be processed independently +/// +/// # Examples +/// +/// ``` +/// use pallas_codec::utils::AnyCbor; +/// +/// let a = (123u16, (456u16, 789u16), 123u16); +/// let data = minicbor::to_vec(a).unwrap(); +/// +/// let (_, any, _): (u16, AnyCbor, u16) = minicbor::decode(&data).unwrap(); +/// let confirm: (u16, u16) = any.into_decode().unwrap(); +/// assert_eq!(confirm, (456u16, 789u16)); +/// ``` +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +pub struct AnyCbor { + inner: Vec, +} + +impl AnyCbor { + pub fn raw_bytes(&self) -> &[u8] { + &self.inner + } + + pub fn unwrap(self) -> Vec { + self.inner + } + + pub fn from_encode(other: T) -> Self + where + T: Encode<()>, + { + let inner = minicbor::to_vec(other).unwrap(); + Self { inner } + } + + pub fn into_decode(self) -> Result + where + for<'b> T: Decode<'b, ()>, + { + minicbor::decode(&self.inner) + } +} + +impl Deref for AnyCbor { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl<'b, C> minicbor::Decode<'b, C> for AnyCbor { + fn decode( + d: &mut minicbor::Decoder<'b>, + _ctx: &mut C, + ) -> Result { + let all = d.input(); + let start = d.position(); + d.skip()?; + let end = d.position(); + + Ok(Self { + inner: Vec::from(&all[start..end]), + }) + } +} + +impl minicbor::Encode for AnyCbor { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.writer_mut() + .write_all(self.raw_bytes()) + .map_err(minicbor::encode::Error::write) + } +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(from = "Option::", into = "Option::")] pub enum Nullable diff --git a/pallas-codec/tests/flat.rs b/pallas-codec/tests/flat.rs new file mode 100644 index 00000000..9d503b61 --- /dev/null +++ b/pallas-codec/tests/flat.rs @@ -0,0 +1,123 @@ +use pallas_codec::flat::filler::Filler; +use pallas_codec::flat::{decode, encode}; +use proptest::prelude::*; + +prop_compose! { + fn arb_big_vec()(size in 255..300, element in any::()) -> Vec { + (0..size).map(|_| element).collect() + } +} + +#[test] +fn encode_bool() { + let bytes = encode(&true).unwrap(); + + assert_eq!(bytes, vec![0b10000001]); + + let decoded: bool = decode(bytes.as_slice()).unwrap(); + + assert!(decoded); + + let bytes = encode(&false).unwrap(); + + assert_eq!(bytes, vec![0b00000001]); + + let decoded: bool = decode(bytes.as_slice()).unwrap(); + + assert!(!decoded); +} + +#[test] +fn encode_u8() { + let bytes = encode(&3_u8).unwrap(); + + assert_eq!(bytes, vec![0b00000011, 0b00000001]); + + let decoded: u8 = decode(bytes.as_slice()).unwrap(); + + assert_eq!(decoded, 3_u8); +} + +proptest! { + #[test] + fn encode_isize(x: isize) { + let bytes = encode(&x).unwrap(); + let decoded: isize = decode(&bytes).unwrap(); + assert_eq!(decoded, x); + } + + #[test] + fn encode_usize(x: usize) { + let bytes = encode(&x).unwrap(); + let decoded: usize = decode(&bytes).unwrap(); + assert_eq!(decoded, x); + } + + #[test] + fn encode_char(c: char) { + let bytes = encode(&c).unwrap(); + let decoded: char = decode(&bytes).unwrap(); + assert_eq!(decoded, c); + } + + #[test] + fn encode_string(str: String) { + let bytes = encode(&str).unwrap(); + let decoded: String = decode(&bytes).unwrap(); + assert_eq!(decoded, str); + } + + #[test] + fn encode_vec_u8(xs: Vec) { + let bytes = encode(&xs).unwrap(); + let decoded: Vec = decode(&bytes).unwrap(); + assert_eq!(decoded, xs); + } + + #[test] + fn encode_big_vec_u8(xs in arb_big_vec()) { + let bytes = encode(&xs).unwrap(); + let decoded: Vec = decode(&bytes).unwrap(); + assert_eq!(decoded, xs); + } + + #[test] + fn encode_arr_u8(xs: Vec) { + let bytes = encode(&xs.as_slice()).unwrap(); + let decoded: Vec = decode(&bytes).unwrap(); + assert_eq!(decoded, xs); + } + + #[test] + fn encode_big_arr_u8(xs in arb_big_vec()) { + let bytes = encode(&xs.as_slice()).unwrap(); + let decoded: Vec = decode(&bytes).unwrap(); + assert_eq!(decoded, xs); + } + + #[test] + fn encode_boxed(c: char) { + let boxed = Box::new(c); + let bytes = encode(&boxed).unwrap(); + let decoded: char = decode(&bytes).unwrap(); + assert_eq!(decoded, c); + } +} + +#[test] +fn encode_filler() { + let bytes = encode(&Filler::FillerEnd).unwrap(); + + assert_eq!(bytes, vec![0b0000001, 0b00000001]); + + let bytes = encode(&Filler::FillerStart(Box::new(Filler::FillerEnd))).unwrap(); + + assert_eq!(bytes, vec![0b0000001, 0b00000001]); + + let bytes = encode(&Filler::FillerStart(Box::new(Filler::FillerStart( + Box::new(Filler::FillerEnd), + )))) + .unwrap(); + + assert_eq!(bytes, vec![0b0000001, 0b00000001]); +} diff --git a/pallas-codec/tests/zigzag.rs b/pallas-codec/tests/zigzag.rs new file mode 100644 index 00000000..2901b745 --- /dev/null +++ b/pallas-codec/tests/zigzag.rs @@ -0,0 +1,18 @@ +use pallas_codec::flat::zigzag::{to_isize, to_usize}; +use proptest::prelude::*; + +proptest! { + #[test] + fn zigzag(i: isize) { + let u = to_usize(i); + let converted_i = to_isize(u); + assert_eq!(converted_i, i); + } + + #[test] + fn zagzig(u: usize) { + let i = to_isize(u); + let converted_u = to_usize(i); + assert_eq!(converted_u, u); + } +} diff --git a/pallas-configs/Cargo.toml b/pallas-configs/Cargo.toml new file mode 100644 index 00000000..c8957f37 --- /dev/null +++ b/pallas-configs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pallas-configs" +description = "Config structs and utilities matching the Haskell implementation" +version = "0.19.1" +edition = "2021" +repository = "https://github.com/txpipe/pallas" +homepage = "https://github.com/txpipe/pallas" +documentation = "https://docs.rs/pallas-configs" +license = "Apache-2.0" +readme = "README.md" +authors = ["Santiago Carmuega "] + +[dependencies] +hex = "0.4.3" +pallas-addresses = { version = "=0.19.1", path = "../pallas-addresses" } +pallas-crypto = { version = "=0.19.1", path = "../pallas-crypto" } +pallas-codec = { version = "=0.19.1", path = "../pallas-codec" } +serde = { version = "1.0.136", optional = true, features = ["derive"] } +serde_json = { version = "1.0.79", optional = true } +base64 = "0.21.2" + +[features] +json = ["serde", "serde_json"] +default = ["json"] diff --git a/pallas-configs/README.md b/pallas-configs/README.md new file mode 100644 index 00000000..e7f28f55 --- /dev/null +++ b/pallas-configs/README.md @@ -0,0 +1,2 @@ +# Pallas Configs + diff --git a/pallas-configs/src/byron.rs b/pallas-configs/src/byron.rs new file mode 100644 index 00000000..5dcea0cd --- /dev/null +++ b/pallas-configs/src/byron.rs @@ -0,0 +1,225 @@ +//! Parsing of Byron configuration data + +use pallas_addresses::ByronAddress; +use pallas_crypto::hash::Hash; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GenesisFile { + pub avvm_distr: HashMap, + pub block_version_data: BlockVersionData, + pub fts_seed: Option, + pub protocol_consts: ProtocolConsts, + pub start_time: u64, + pub boot_stakeholders: HashMap, + pub heavy_delegation: HashMap, + pub non_avvm_balances: HashMap, + pub vss_certs: Option>, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BlockVersionData { + pub heavy_del_thd: String, + pub max_block_size: String, + pub max_header_size: String, + pub max_proposal_size: String, + pub max_tx_size: String, + pub mpc_thd: String, + pub script_version: u32, + pub slot_duration: String, + pub softfork_rule: SoftForkRule, + pub tx_fee_policy: TxFeePolicy, + pub unlock_stake_epoch: String, + pub update_implicit: String, + pub update_proposal_thd: String, + pub update_vote_thd: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProtocolConsts { + pub k: usize, + pub protocol_magic: u32, + #[serde(rename = "vssMaxTTL")] + pub vss_max_ttl: Option, + #[serde(rename = "vssMinTTL")] + pub vss_min_ttl: Option, +} + +pub type BootStakeWeight = u16; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HeavyDelegation { + pub issuer_pk: String, + pub delegate_pk: String, + pub cert: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct VssCert { + pub vss_key: String, + // TODO: is this size fine? + pub expiry_epoch: u32, + pub signature: String, + pub signing_key: String, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SoftForkRule { + pub init_thd: String, + pub min_thd: String, + pub thd_decrement: String, +} + +#[derive(Debug, Deserialize)] +pub struct TxFeePolicy { + pub multiplier: String, + pub summand: String, +} + +pub fn from_file(path: &std::path::Path) -> Result { + let file = std::fs::File::open(path)?; + let reader = std::io::BufReader::new(file); + let parsed: GenesisFile = serde_json::from_reader(reader)?; + + Ok(parsed) +} + +use base64::Engine; + +pub type GenesisUtxo = (Hash<32>, ByronAddress, u64); + +pub fn genesis_avvm_utxos(config: &GenesisFile) -> Vec { + config + .avvm_distr + .iter() + .map(|(pubkey, amount)| { + let amount = amount.parse().unwrap(); + let pubkey = base64::engine::general_purpose::URL_SAFE + .decode(pubkey) + .unwrap(); + + let pubkey = pallas_crypto::key::ed25519::PublicKey::try_from(&pubkey[..]).unwrap(); + + // TODO: network tag + //let network_tag = Some(config.protocol_consts.protocol_magic); + let network_tag = None; + + let addr = pallas_addresses::byron::AddressPayload::new_redeem(pubkey, network_tag); + + let addr: pallas_addresses::ByronAddress = addr.into(); + + let txid = pallas_crypto::hash::Hasher::<256>::hash_cbor(&addr); + + (txid, addr, amount) + }) + .collect() +} + +pub fn genesis_non_avvm_utxos(config: &GenesisFile) -> Vec { + config + .non_avvm_balances + .iter() + .map(|(addr, amount)| { + let amount = amount.parse().unwrap(); + let addr = ByronAddress::from_base58(addr).unwrap(); + + let txid = pallas_crypto::hash::Hasher::<256>::hash_cbor(&addr); + + (txid, addr, amount) + }) + .collect() +} + +pub fn genesis_utxos(config: &GenesisFile) -> Vec { + let avvm = genesis_avvm_utxos(config); + let non_avvm = genesis_non_avvm_utxos(config); + + [avvm, non_avvm].concat().to_vec() +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + fn load_test_data_config(network: &str) -> GenesisFile { + let path = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("..") + .join("test_data") + .join(format!("{network}-byron-genesis.json")); + + from_file(&path).unwrap() + } + + #[test] + fn test_preview_json_loads() { + load_test_data_config("preview"); + } + + #[test] + fn test_mainnet_json_loads() { + load_test_data_config("mainnet"); + } + + fn utxo_exists(set: &[GenesisUtxo], expected: GenesisUtxo) -> bool { + set.iter().any(|(hash, addr, amount)| { + hash.eq(&expected.0) && addr.eq(&expected.1) && amount.eq(&expected.2) + }) + } + + fn genesis_utxo_from_raw(hash_hex: &str, addr_base58: &str, amount: u64) -> GenesisUtxo { + ( + Hash::from_str(hash_hex).unwrap(), + ByronAddress::from_base58(addr_base58).unwrap(), + amount, + ) + } + + #[test] + fn test_preview_non_avvm_utxos() { + let f = load_test_data_config("preview"); + + let utxos = super::genesis_non_avvm_utxos(&f); + assert_eq!(utxos.len(), 8); + + // check known tx as seen: https://preview.cexplorer.io/tx/4843cf2e582b2f9ce37600e5ab4cc678991f988f8780fed05407f9537f7712bd + let expected = genesis_utxo_from_raw( + "4843cf2e582b2f9ce37600e5ab4cc678991f988f8780fed05407f9537f7712bd", + "FHnt4NL7yPXvDWHa8bVs73UEUdJd64VxWXSFNqetECtYfTd9TtJguJ14Lu3feth", + 30_000_000_000_000_000, + ); + + assert!(utxo_exists(&utxos, expected)); + } + + #[test] + pub fn test_mainnet_avvm_utxos() { + let f = load_test_data_config("mainnet"); + + let utxos = super::genesis_non_avvm_utxos(&f); + + // there aren't non-avvm utxos in mainnet + assert!(utxos.is_empty()); + + let utxos = super::genesis_avvm_utxos(&f); + + assert_eq!(utxos.len(), 14505); + + // check known tx as seen: https://cexplorer.io/tx/0ae3da29711600e94a33fb7441d2e76876a9a1e98b5ebdefbf2e3bc535617616 + let expected = genesis_utxo_from_raw( + "0ae3da29711600e94a33fb7441d2e76876a9a1e98b5ebdefbf2e3bc535617616", + "Ae2tdPwUPEZKQuZh2UndEoTKEakMYHGNjJVYmNZgJk2qqgHouxDsA5oT83n", + 2_463_071_701_000_000, + ); + + assert!(utxo_exists(&utxos, expected)); + } +} diff --git a/pallas-configs/src/lib.rs b/pallas-configs/src/lib.rs new file mode 100644 index 00000000..a000d3c5 --- /dev/null +++ b/pallas-configs/src/lib.rs @@ -0,0 +1,3 @@ +//! Genesis data structs and utilities + +pub mod byron; diff --git a/pallas-crypto/Cargo.toml b/pallas-crypto/Cargo.toml index a01f7785..5d139ad2 100644 --- a/pallas-crypto/Cargo.toml +++ b/pallas-crypto/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pallas-crypto" description = "Cryptographic primitives for Cardano" -version = "0.19.0-alpha.1" +version = "0.19.1" edition = "2021" repository = "https://github.com/txpipe/pallas" homepage = "https://github.com/txpipe/pallas" @@ -15,7 +15,7 @@ hex = "0.4" cryptoxide = { version = "0.4.1" } thiserror = "1.0" rand_core = "0.6" -pallas-codec = { version = "0.19.0-alpha.0", path = "../pallas-codec" } +pallas-codec = { version = "=0.19.1", path = "../pallas-codec" } serde = "1.0.143" [dev-dependencies] diff --git a/pallas-crypto/src/hash/hash.rs b/pallas-crypto/src/hash/hash.rs index afb3507d..5cf5485a 100644 --- a/pallas-crypto/src/hash/hash.rs +++ b/pallas-crypto/src/hash/hash.rs @@ -22,6 +22,14 @@ impl From<[u8; BYTES]> for Hash { } } +impl From<&[u8]> for Hash { + fn from(value: &[u8]) -> Self { + let mut hash = [0; BYTES]; + hash.copy_from_slice(value); + Self::new(hash) + } +} + impl AsRef<[u8]> for Hash { #[inline] fn as_ref(&self) -> &[u8] { diff --git a/pallas-network/Cargo.toml b/pallas-network/Cargo.toml index 78d4c9f5..2ffe86d6 100644 --- a/pallas-network/Cargo.toml +++ b/pallas-network/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pallas-network" description = "Ouroboros networking stack using async IO" -version = "0.19.0-alpha.1" +version = "0.19.1" edition = "2021" repository = "https://github.com/txpipe/pallas" homepage = "https://github.com/txpipe/pallas" @@ -17,12 +17,16 @@ authors = [ byteorder = "1.4.3" hex = "0.4.3" itertools = "0.11.0" -pallas-codec = { version = "0.19.0-alpha.0", path = "../pallas-codec" } -pallas-crypto = { version = "0.19.0-alpha.0", path = "../pallas-crypto" } +pallas-codec = { version = "=0.19.1", path = "../pallas-codec" } +pallas-crypto = { version = "=0.19.1", path = "../pallas-crypto" } thiserror = "1.0.31" -tokio = { version = "1", features = ["net", "io-util", "time", "sync"] } +tokio = { version = "1", features = ["net", "io-util", "time", "sync", "macros"] } tracing = "0.1.37" +[target.'cfg(windows)'.dependencies] +tokio-named-pipes = "0.1.0" +windows-sys = "0.48.0" + [dev-dependencies] tracing-subscriber = "0.3.16" tokio = { version = "1", features = ["full"] } diff --git a/pallas-network/src/facades.rs b/pallas-network/src/facades.rs index 1e5fd994..8ba4f366 100644 --- a/pallas-network/src/facades.rs +++ b/pallas-network/src/facades.rs @@ -1,13 +1,21 @@ use std::path::Path; use thiserror::Error; +use tokio::net::TcpListener; use tokio::task::JoinHandle; use tracing::{debug, error}; +#[cfg(unix)] +use tokio::net::UnixListener; + +use crate::miniprotocols::handshake::{n2c, n2n, Confirmation, VersionNumber, VersionTable}; + +use crate::miniprotocols::PROTOCOL_N2N_HANDSHAKE; use crate::{ miniprotocols::{ blockfetch, chainsync, handshake, localstate, PROTOCOL_N2C_CHAIN_SYNC, - PROTOCOL_N2C_HANDSHAKE, PROTOCOL_N2C_STATE_QUERY, + PROTOCOL_N2C_HANDSHAKE, PROTOCOL_N2C_STATE_QUERY, PROTOCOL_N2N_BLOCK_FETCH, + PROTOCOL_N2N_CHAIN_SYNC, }, multiplexer::{self, Bearer}, }; @@ -24,11 +32,12 @@ pub enum Error { IncompatibleVersion, } +/// Client of N2N Ouroboros pub struct PeerClient { - plexer_handle: JoinHandle>, + pub plexer_handle: JoinHandle>, pub handshake: handshake::Confirmation, - chainsync: chainsync::N2NClient, - blockfetch: blockfetch::Client, + pub chainsync: chainsync::N2NClient, + pub blockfetch: blockfetch::Client, } impl PeerClient { @@ -80,23 +89,76 @@ impl PeerClient { } } -pub struct NodeClient { - plexer_handle: JoinHandle>, - pub handshake: handshake::Confirmation, - chainsync: chainsync::N2CClient, - statequery: localstate::ClientV10, +/// Server of N2N Ouroboros +pub struct PeerServer { + pub plexer_handle: JoinHandle>, + pub version: (VersionNumber, n2n::VersionData), + pub chainsync: chainsync::N2NServer, + pub blockfetch: blockfetch::Server, } -impl NodeClient { +impl PeerServer { + pub async fn accept(listener: &TcpListener, magic: u64) -> Result { + let (bearer, _) = Bearer::accept_tcp(listener) + .await + .map_err(Error::ConnectFailure)?; - #[cfg(not(target_os = "windows"))] - pub async fn connect(path: impl AsRef, magic: u64) -> Result { - debug!("connecting"); + let mut server_plexer = multiplexer::Plexer::new(bearer); - let bearer = Bearer::connect_unix(path) + let hs_channel = server_plexer.subscribe_server(PROTOCOL_N2N_HANDSHAKE); + let cs_channel = server_plexer.subscribe_server(PROTOCOL_N2N_CHAIN_SYNC); + let bf_channel = server_plexer.subscribe_server(PROTOCOL_N2N_BLOCK_FETCH); + + let mut server_hs: handshake::Server = handshake::Server::new(hs_channel); + let server_cs = chainsync::N2NServer::new(cs_channel); + let server_bf = blockfetch::Server::new(bf_channel); + + let plexer_handle = tokio::spawn(async move { server_plexer.run().await }); + + let accepted_version = server_hs + .handshake(n2n::VersionTable::v7_and_above(magic)) .await - .map_err(Error::ConnectFailure)?; + .map_err(Error::HandshakeProtocol)?; + + if let Some(ver) = accepted_version { + Ok(Self { + plexer_handle, + version: ver, + chainsync: server_cs, + blockfetch: server_bf, + }) + } else { + plexer_handle.abort(); + Err(Error::IncompatibleVersion) + } + } + + pub fn chainsync(&mut self) -> &mut chainsync::N2NServer { + &mut self.chainsync + } + + pub fn blockfetch(&mut self) -> &mut blockfetch::Server { + &mut self.blockfetch + } + + pub fn abort(&mut self) { + self.plexer_handle.abort(); + } +} + +/// Client of N2C Ouroboros +pub struct NodeClient { + pub plexer_handle: JoinHandle>, + pub handshake: handshake::Confirmation, + pub chainsync: chainsync::N2CClient, + pub statequery: localstate::Client, +} +impl NodeClient { + async fn connect_bearer( + bearer: Bearer, + versions: VersionTable, + ) -> Result { let mut plexer = multiplexer::Plexer::new(bearer); let hs_channel = plexer.subscribe_client(PROTOCOL_N2C_HANDSHAKE); @@ -105,7 +167,6 @@ impl NodeClient { let plexer_handle = tokio::spawn(async move { plexer.run().await }); - let versions = handshake::n2c::VersionTable::v10_and_above(magic); let mut client = handshake::Client::new(hs_channel); let handshake = client @@ -126,11 +187,140 @@ impl NodeClient { }) } + #[cfg(unix)] + pub async fn connect(path: impl AsRef, magic: u64) -> Result { + debug!("connecting"); + + let bearer = Bearer::connect_unix(path) + .await + .map_err(Error::ConnectFailure)?; + + let versions = handshake::n2c::VersionTable::v10_and_above(magic); + + Self::connect_bearer(bearer, versions).await + } + + #[cfg(windows)] + pub async fn connect( + pipe_name: impl AsRef, + magic: u64, + ) -> Result { + debug!("connecting"); + + let bearer = Bearer::connect_named_pipe(pipe_name) + .await + .map_err(Error::ConnectFailure)?; + + let versions = handshake::n2c::VersionTable::v10_and_above(magic); + + Self::connect_bearer(bearer, versions).await + } + + #[cfg(unix)] + pub async fn handshake_query( + path: impl AsRef, + magic: u64, + ) -> Result { + debug!("connecting"); + + let bearer = Bearer::connect_unix(path) + .await + .map_err(Error::ConnectFailure)?; + + let mut plexer = multiplexer::Plexer::new(bearer); + + let hs_channel = plexer.subscribe_client(PROTOCOL_N2C_HANDSHAKE); + + let plexer_handle = tokio::spawn(async move { plexer.run().await }); + + let versions = handshake::n2c::VersionTable::v15_with_query(magic); + let mut client = handshake::Client::new(hs_channel); + + let handshake = client + .handshake(versions) + .await + .map_err(Error::HandshakeProtocol)?; + + match handshake { + Confirmation::Accepted(_, _) => { + error!("handshake accepted when we expected query reply"); + Err(Error::HandshakeProtocol(handshake::Error::InvalidInbound)) + } + Confirmation::Rejected(reason) => { + error!(?reason, "handshake refused"); + Err(Error::IncompatibleVersion) + } + Confirmation::QueryReply(version_table) => { + plexer_handle.abort(); + Ok(version_table) + } + } + } + pub fn chainsync(&mut self) -> &mut chainsync::N2CClient { &mut self.chainsync } - pub fn statequery(&mut self) -> &mut localstate::ClientV10 { + pub fn statequery(&mut self) -> &mut localstate::Client { + &mut self.statequery + } + + pub fn abort(&mut self) { + self.plexer_handle.abort(); + } +} + +/// Server of N2C Ouroboros. +#[cfg(unix)] +pub struct NodeServer { + pub plexer_handle: JoinHandle>, + pub version: (VersionNumber, n2c::VersionData), + pub chainsync: chainsync::N2CServer, + pub statequery: localstate::Server, +} + +#[cfg(unix)] +impl NodeServer { + pub async fn accept(listener: &UnixListener, magic: u64) -> Result { + let (bearer, _) = Bearer::accept_unix(listener) + .await + .map_err(Error::ConnectFailure)?; + + let mut server_plexer = multiplexer::Plexer::new(bearer); + + let hs_channel = server_plexer.subscribe_server(PROTOCOL_N2C_HANDSHAKE); + let cs_channel = server_plexer.subscribe_server(PROTOCOL_N2C_CHAIN_SYNC); + let sq_channel = server_plexer.subscribe_server(PROTOCOL_N2C_STATE_QUERY); + + let mut server_hs: handshake::Server = handshake::Server::new(hs_channel); + let server_cs = chainsync::N2CServer::new(cs_channel); + let server_sq = localstate::Server::new(sq_channel); + + let plexer_handle = tokio::spawn(async move { server_plexer.run().await }); + + let accepted_version = server_hs + .handshake(n2c::VersionTable::v10_and_above(magic)) + .await + .map_err(Error::HandshakeProtocol)?; + + if let Some(ver) = accepted_version { + Ok(Self { + plexer_handle, + version: ver, + chainsync: server_cs, + statequery: server_sq, + }) + } else { + plexer_handle.abort(); + Err(Error::IncompatibleVersion) + } + } + + pub fn chainsync(&mut self) -> &mut chainsync::N2CServer { + &mut self.chainsync + } + + pub fn statequery(&mut self) -> &mut localstate::Server { &mut self.statequery } diff --git a/pallas-network/src/miniprotocols/README.md b/pallas-network/src/miniprotocols/README.md index 69c23a07..2fc6cb9d 100644 --- a/pallas-network/src/miniprotocols/README.md +++ b/pallas-network/src/miniprotocols/README.md @@ -1,6 +1,6 @@ # Pallas Mini-protocols -This crate provides an implementation of the different Ouroboros mini-protocols as defined in the [The Shelley Networking Protocol](https://hydra.iohk.io/build/1070091/download/1/network.pdf#chapter.3) specs. +This crate provides an implementation of the different Ouroboros mini-protocols as defined in the [The Shelley Networking Protocol](https://input-output-hk.github.io/ouroboros-network/pdfs/network-spec/network-spec.pdf#chapter.3) specs. ## Architectural Decisions @@ -13,14 +13,14 @@ The following architectural decisions were made for this particular Rust impleme ## Development Status | mini-protocol | initiator | responder | -| ------------------------------------------- | --------- | --------- | +| ------------------------------------------- |-----------| --------- | | block-fetch | done | planned | | chain-sync | done | planned | | [handshake](src/handshake/README.md) | done | done | | local-state | done | planned | | [tx-submission](src/txsubmission/README.md) | done | done | | local tx monitor | done | planned | -| local-tx-submission | ongoing | planned | +| local-tx-submission | done | planned | ## Implementation Details diff --git a/pallas-network/src/miniprotocols/blockfetch/client.rs b/pallas-network/src/miniprotocols/blockfetch/client.rs index adeb1f60..abaff776 100644 --- a/pallas-network/src/miniprotocols/blockfetch/client.rs +++ b/pallas-network/src/miniprotocols/blockfetch/client.rs @@ -1,5 +1,5 @@ use thiserror::Error; -use tracing::{debug, info, warn}; +use tracing::{debug, warn}; use crate::miniprotocols::common::Point; use crate::multiplexer; @@ -7,7 +7,7 @@ use crate::multiplexer; use super::{Message, State}; #[derive(Error, Debug)] -pub enum Error { +pub enum ClientError { #[error("attempted to receive message while agency is ours")] AgencyIsOurs, @@ -74,57 +74,60 @@ impl Client { } } - fn assert_agency_is_ours(&self) -> Result<(), Error> { + fn assert_agency_is_ours(&self) -> Result<(), ClientError> { if !self.has_agency() { - Err(Error::AgencyIsTheirs) + Err(ClientError::AgencyIsTheirs) } else { Ok(()) } } - fn assert_agency_is_theirs(&self) -> Result<(), Error> { + fn assert_agency_is_theirs(&self) -> Result<(), ClientError> { if self.has_agency() { - Err(Error::AgencyIsOurs) + Err(ClientError::AgencyIsOurs) } else { Ok(()) } } - fn assert_outbound_state(&self, msg: &Message) -> Result<(), Error> { + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ClientError> { match (&self.0, msg) { (State::Idle, Message::RequestRange { .. }) => Ok(()), (State::Idle, Message::ClientDone) => Ok(()), - _ => Err(Error::InvalidOutbound), + _ => Err(ClientError::InvalidOutbound), } } - fn assert_inbound_state(&self, msg: &Message) -> Result<(), Error> { + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ClientError> { match (&self.0, msg) { (State::Busy, Message::StartBatch) => Ok(()), (State::Busy, Message::NoBlocks) => Ok(()), (State::Streaming, Message::Block { .. }) => Ok(()), (State::Streaming, Message::BatchDone) => Ok(()), - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } - pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ClientError> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ClientError::Plexer)?; Ok(()) } - pub async fn recv_message(&mut self) -> Result { + pub async fn recv_message(&mut self) -> Result { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; + let msg = self.1.recv_full_msg().await.map_err(ClientError::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) } - pub async fn send_request_range(&mut self, range: (Point, Point)) -> Result<(), Error> { + pub async fn send_request_range(&mut self, range: (Point, Point)) -> Result<(), ClientError> { let msg = Message::RequestRange { range }; self.send_message(&msg).await?; self.0 = State::Busy; @@ -132,10 +135,10 @@ impl Client { Ok(()) } - pub async fn recv_while_busy(&mut self) -> Result { + pub async fn recv_while_busy(&mut self) -> Result { match self.recv_message().await? { Message::StartBatch => { - info!("batch start"); + debug!("batch start"); self.0 = State::Streaming; Ok(Some(())) } @@ -144,7 +147,7 @@ impl Client { self.0 = State::Idle; Ok(None) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } @@ -154,7 +157,7 @@ impl Client { /// /// * `range` - A tuple of two `Point` instances representing the start and /// end of the requested block range. - pub async fn request_range(&mut self, range: Range) -> Result { + pub async fn request_range(&mut self, range: Range) -> Result { self.send_request_range(range).await?; debug!("range requested"); self.recv_while_busy().await @@ -164,7 +167,7 @@ impl Client { /// /// Returns a block's body if a block is received, or `None` if the /// streaming has ended. - pub async fn recv_while_streaming(&mut self) -> Result, Error> { + pub async fn recv_while_streaming(&mut self) -> Result, ClientError> { debug!("waiting for stream"); match self.recv_message().await? { @@ -173,7 +176,7 @@ impl Client { self.0 = State::Idle; Ok(None) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } @@ -185,20 +188,20 @@ impl Client { /// /// Returns the block's body if the block is found, or an `Error` if the /// block is not found or an invalid message is received. - pub async fn fetch_single(&mut self, point: Point) -> Result { + pub async fn fetch_single(&mut self, point: Point) -> Result { self.request_range((point.clone(), point)) .await? - .ok_or(Error::NoBlocks)?; + .ok_or(ClientError::NoBlocks)?; let body = self .recv_while_streaming() .await? - .ok_or(Error::InvalidInbound)?; + .ok_or(ClientError::InvalidInbound)?; debug!("body received"); match self.recv_while_streaming().await? { - Some(_) => Err(Error::InvalidInbound), + Some(_) => Err(ClientError::InvalidInbound), None => Ok(body), } } @@ -212,8 +215,10 @@ impl Client { /// /// Returns a vector of block bodies for the requested range, or an `Error` /// if the range is not found. - pub async fn fetch_range(&mut self, range: Range) -> Result, Error> { - self.request_range(range).await?.ok_or(Error::NoBlocks)?; + pub async fn fetch_range(&mut self, range: Range) -> Result, ClientError> { + self.request_range(range) + .await? + .ok_or(ClientError::NoBlocks)?; let mut all = vec![]; @@ -230,7 +235,7 @@ impl Client { /// /// Returns `Ok(())` if the message is sent successfully, or an `Error` if /// the agency is not ours. - pub async fn send_done(&mut self) -> Result<(), Error> { + pub async fn send_done(&mut self) -> Result<(), ClientError> { let msg = Message::ClientDone; self.send_message(&msg).await?; self.0 = State::Done; diff --git a/pallas-network/src/miniprotocols/blockfetch/mod.rs b/pallas-network/src/miniprotocols/blockfetch/mod.rs index 63262352..26cb0507 100644 --- a/pallas-network/src/miniprotocols/blockfetch/mod.rs +++ b/pallas-network/src/miniprotocols/blockfetch/mod.rs @@ -3,7 +3,9 @@ mod client; mod codec; mod protocol; +mod server; pub use client::*; pub use codec::*; pub use protocol::*; +pub use server::*; diff --git a/pallas-network/src/miniprotocols/blockfetch/server.rs b/pallas-network/src/miniprotocols/blockfetch/server.rs new file mode 100644 index 00000000..00a72156 --- /dev/null +++ b/pallas-network/src/miniprotocols/blockfetch/server.rs @@ -0,0 +1,193 @@ +use thiserror::Error; + +use crate::multiplexer; + +use super::{Body, Message, Range, State}; + +#[derive(Error, Debug)] +pub enum ServerError { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("inbound message is not valid for current state")] + InvalidInbound, + + #[error("outbound message is not valid for current state")] + InvalidOutbound, + + #[error("error while sending or receiving data through the multiplexer")] + Plexer(multiplexer::Error), +} + +#[derive(Debug)] +pub struct BlockRequest(pub Range); + +/// Represents the server for the BlockFetch mini-protocol. +pub struct Server(State, multiplexer::ChannelBuffer); + +impl Server { + /// Create a new BlockFetch server from a multiplexer agent channel. + /// + /// # Arguments + /// + /// * `channel` - A multiplexer agent channel used for communication with + /// the server. + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self(State::Idle, multiplexer::ChannelBuffer::new(channel)) + } + + /// Get the current state of the server. + /// + /// Returns the current state of the server. + pub fn state(&self) -> &State { + &self.0 + } + + /// Check if the server is done. + /// + /// Returns true if server is in the `Done` state, false otherwise. + pub fn is_done(&self) -> bool { + self.0 == State::Done + } + + fn has_agency(&self) -> bool { + match self.state() { + State::Idle => false, + State::Busy => true, + State::Streaming => true, + State::Done => false, + } + } + + fn assert_agency_is_ours(&self) -> Result<(), ServerError> { + if !self.has_agency() { + Err(ServerError::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), ServerError> { + if self.has_agency() { + Err(ServerError::AgencyIsOurs) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ServerError> { + match (&self.0, msg) { + (State::Busy, Message::NoBlocks) => Ok(()), + (State::Busy, Message::StartBatch) => Ok(()), + (State::Streaming, Message::Block { .. }) => Ok(()), + (State::Streaming, Message::BatchDone) => Ok(()), + _ => Err(ServerError::InvalidOutbound), + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ServerError> { + match (&self.0, msg) { + (State::Idle, Message::RequestRange { .. }) => Ok(()), + (State::Idle, Message::ClientDone) => Ok(()), + _ => Err(ServerError::InvalidInbound), + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ServerError> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ServerError::Plexer)?; + + Ok(()) + } + + pub async fn recv_message(&mut self) -> Result { + self.assert_agency_is_theirs()?; + let msg = self.1.recv_full_msg().await.map_err(ServerError::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + pub async fn send_start_batch(&mut self) -> Result<(), ServerError> { + let msg = Message::StartBatch; + self.send_message(&msg).await?; + self.0 = State::Streaming; + + Ok(()) + } + + pub async fn send_no_blocks(&mut self) -> Result<(), ServerError> { + let msg = Message::NoBlocks; + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_block(&mut self, body: Body) -> Result<(), ServerError> { + let msg = Message::Block { body }; + self.send_message(&msg).await?; + + Ok(()) + } + + pub async fn send_batch_done(&mut self) -> Result<(), ServerError> { + let msg = Message::BatchDone; + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + /// Receive a message from the client while the miniprotocol is in the + /// `Idle` state. + /// + /// If the message is a `RequestRange`, return the requested range and + /// progess the server state to `Busy`. If the message is a `ClientDone`, + /// return None and progress the server state to `Done`. For any other + /// incoming message type return an `Error`. + pub async fn recv_while_idle(&mut self) -> Result, ServerError> { + match self.recv_message().await? { + Message::RequestRange { range } => { + self.0 = State::Busy; + + Ok(Some(BlockRequest(range))) + } + Message::ClientDone => { + self.0 = State::Done; + + Ok(None) + } + _ => Err(ServerError::InvalidInbound), + } + } + + /// Return a range of blocks to the client, starting in the `Busy` state and + /// progressing the state machine as required to send all the blocks to the + /// client. + /// + /// # Arguments + /// + /// * `blocks` - Ordered list of block bodies corresponding to the client's + /// requested range. + pub async fn send_block_range(&mut self, blocks: Vec) -> Result<(), ServerError> { + if blocks.is_empty() { + self.send_no_blocks().await + } else { + self.send_start_batch().await?; + + for block in blocks { + self.send_block(block).await?; + } + + self.send_batch_done().await + } + } +} diff --git a/pallas-network/src/miniprotocols/chainsync/client.rs b/pallas-network/src/miniprotocols/chainsync/client.rs index 593859b5..f7eb54cd 100644 --- a/pallas-network/src/miniprotocols/chainsync/client.rs +++ b/pallas-network/src/miniprotocols/chainsync/client.rs @@ -6,10 +6,10 @@ use tracing::debug; use crate::miniprotocols::Point; use crate::multiplexer; -use super::{BlockContent, HeaderContent, Message, State, Tip}; +use super::{BlockContent, HeaderContent, IntersectResponse, Message, State, Tip}; #[derive(Error, Debug)] -pub enum Error { +pub enum ClientError { #[error("attempted to receive message while agency is ours")] AgencyIsOurs, @@ -29,8 +29,6 @@ pub enum Error { Plexer(multiplexer::Error), } -pub type IntersectResponse = (Option, Tip); - #[derive(Debug)] pub enum NextResponse { RollForward(CONTENT, Tip), @@ -81,32 +79,32 @@ where } } - fn assert_agency_is_ours(&self) -> Result<(), Error> { + fn assert_agency_is_ours(&self) -> Result<(), ClientError> { if !self.has_agency() { - Err(Error::AgencyIsTheirs) + Err(ClientError::AgencyIsTheirs) } else { Ok(()) } } - fn assert_agency_is_theirs(&self) -> Result<(), Error> { + fn assert_agency_is_theirs(&self) -> Result<(), ClientError> { if self.has_agency() { - Err(Error::AgencyIsOurs) + Err(ClientError::AgencyIsOurs) } else { Ok(()) } } - fn assert_outbound_state(&self, msg: &Message) -> Result<(), Error> { + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ClientError> { match (&self.0, msg) { (State::Idle, Message::RequestNext) => Ok(()), (State::Idle, Message::FindIntersect(_)) => Ok(()), (State::Idle, Message::Done) => Ok(()), - _ => Err(Error::InvalidOutbound), + _ => Err(ClientError::InvalidOutbound), } } - fn assert_inbound_state(&self, msg: &Message) -> Result<(), Error> { + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ClientError> { match (&self.0, msg) { (State::CanAwait, Message::RollForward(_, _)) => Ok(()), (State::CanAwait, Message::RollBackward(_, _)) => Ok(()), @@ -115,7 +113,7 @@ where (State::MustReply, Message::RollBackward(_, _)) => Ok(()), (State::Intersect, Message::IntersectFound(_, _)) => Ok(()), (State::Intersect, Message::IntersectNotFound(_)) => Ok(()), - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } @@ -129,11 +127,14 @@ where /// /// Returns an error if the agency is not ours or if the outbound state is /// invalid. - pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ClientError> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ClientError::Plexer)?; Ok(()) } @@ -144,10 +145,10 @@ where /// /// Returns an error if the agency is not theirs or if the inbound state is /// invalid. - pub async fn recv_message(&mut self) -> Result, Error> { + pub async fn recv_message(&mut self) -> Result, ClientError> { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; + let msg = self.1.recv_full_msg().await.map_err(ClientError::Plexer)?; self.assert_inbound_state(&msg)?; @@ -165,7 +166,7 @@ where /// /// Returns an error if the message cannot be sent or if it's not valid for /// the current state of the client. - pub async fn send_find_intersect(&mut self, points: Vec) -> Result<(), Error> { + pub async fn send_find_intersect(&mut self, points: Vec) -> Result<(), ClientError> { let msg = Message::FindIntersect(points); self.send_message(&msg).await?; self.0 = State::Intersect; @@ -180,7 +181,7 @@ where /// # Errors /// /// Returns an error if the inbound message is invalid. - pub async fn recv_intersect_response(&mut self) -> Result { + pub async fn recv_intersect_response(&mut self) -> Result { debug!("waiting for intersect response"); match self.recv_message().await? { @@ -192,7 +193,7 @@ where self.0 = State::Idle; Ok((None, tip)) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } @@ -207,12 +208,15 @@ where /// /// Returns an error if the intersection point cannot be found or if there /// is a communication error. - pub async fn find_intersect(&mut self, points: Vec) -> Result { + pub async fn find_intersect( + &mut self, + points: Vec, + ) -> Result { self.send_find_intersect(points).await?; self.recv_intersect_response().await } - pub async fn send_request_next(&mut self) -> Result<(), Error> { + pub async fn send_request_next(&mut self) -> Result<(), ClientError> { let msg = Message::RequestNext; self.send_message(&msg).await?; self.0 = State::CanAwait; @@ -225,7 +229,7 @@ where /// # Errors /// /// Returns an error if the inbound message is invalid. - pub async fn recv_while_can_await(&mut self) -> Result, Error> { + pub async fn recv_while_can_await(&mut self) -> Result, ClientError> { match self.recv_message().await? { Message::AwaitReply => { self.0 = State::MustReply; @@ -239,7 +243,7 @@ where self.0 = State::Idle; Ok(NextResponse::RollBackward(a, b)) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } @@ -248,7 +252,7 @@ where /// # Errors /// /// Returns an error if the inbound message is invalid. - pub async fn recv_while_must_reply(&mut self) -> Result, Error> { + pub async fn recv_while_must_reply(&mut self) -> Result, ClientError> { match self.recv_message().await? { Message::RollForward(a, b) => { self.0 = State::Idle; @@ -258,7 +262,7 @@ where self.0 = State::Idle; Ok(NextResponse::RollBackward(a, b)) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } @@ -268,7 +272,7 @@ where /// /// Returns an error if the message cannot be sent or if the state is not /// idle. - pub async fn request_next(&mut self) -> Result, Error> { + pub async fn request_next(&mut self) -> Result, ClientError> { debug!("requesting next block"); self.send_request_next().await?; @@ -282,12 +286,12 @@ where /// /// Returns an error if the intersection point cannot be found or if there /// is a communication error. - pub async fn intersect_origin(&mut self) -> Result { + pub async fn intersect_origin(&mut self) -> Result { debug!("intersecting origin"); let (point, _) = self.find_intersect(vec![Point::Origin]).await?; - point.ok_or(Error::IntersectionNotFound) + point.ok_or(ClientError::IntersectionNotFound) } /// Attempts to intersect the chain at the latest known tip @@ -296,17 +300,17 @@ where /// /// Returns an error if the intersection point cannot be found or if there /// is a communication error. - pub async fn intersect_tip(&mut self) -> Result { + pub async fn intersect_tip(&mut self) -> Result { let (_, Tip(point, _)) = self.find_intersect(vec![Point::Origin]).await?; debug!(?point, "found tip value"); let (point, _) = self.find_intersect(vec![point]).await?; - point.ok_or(Error::IntersectionNotFound) + point.ok_or(ClientError::IntersectionNotFound) } - pub async fn send_done(&mut self) -> Result<(), Error> { + pub async fn send_done(&mut self) -> Result<(), ClientError> { let msg = Message::Done; self.send_message(&msg).await?; self.0 = State::Done; diff --git a/pallas-network/src/miniprotocols/chainsync/codec.rs b/pallas-network/src/miniprotocols/chainsync/codec.rs index ba239235..9a123d5f 100644 --- a/pallas-network/src/miniprotocols/chainsync/codec.rs +++ b/pallas-network/src/miniprotocols/chainsync/codec.rs @@ -1,4 +1,5 @@ use pallas_codec::minicbor; +use pallas_codec::minicbor::encode::Error; use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; use super::{BlockContent, HeaderContent, Message, SkippedContent, Tip}; @@ -167,10 +168,32 @@ impl<'b> Decode<'b, ()> for HeaderContent { impl Encode<()> for HeaderContent { fn encode( &self, - _e: &mut Encoder, + e: &mut Encoder, _ctx: &mut (), ) -> Result<(), encode::Error> { - todo!() + e.array(2)?; + e.u8(self.variant)?; + + // variant 0 is byron + if self.variant == 0 { + e.array(2)?; + + if let Some((a, b)) = self.byron_prefix { + e.array(2)?; + e.u8(a)?; + e.u64(b)?; + } else { + return Err(Error::message("header variant 0 but no byron prefix")); + } + + e.tag(minicbor::data::Tag::Cbor)?; + e.bytes(&self.cbor)?; + } else { + e.tag(minicbor::data::Tag::Cbor)?; + e.bytes(&self.cbor)?; + } + + Ok(()) } } @@ -185,10 +208,13 @@ impl<'b> Decode<'b, ()> for BlockContent { impl Encode<()> for BlockContent { fn encode( &self, - _e: &mut Encoder, + e: &mut Encoder, _ctx: &mut (), ) -> Result<(), encode::Error> { - todo!() + e.tag(minicbor::data::Tag::Cbor)?; + e.bytes(&self.0)?; + + Ok(()) } } @@ -202,9 +228,11 @@ impl<'b> Decode<'b, ()> for SkippedContent { impl Encode<()> for SkippedContent { fn encode( &self, - _e: &mut Encoder, + e: &mut Encoder, _ctx: &mut (), ) -> Result<(), encode::Error> { - todo!() + e.null()?; + + Ok(()) } } diff --git a/pallas-network/src/miniprotocols/chainsync/mod.rs b/pallas-network/src/miniprotocols/chainsync/mod.rs index 2ad863ea..6b732fec 100644 --- a/pallas-network/src/miniprotocols/chainsync/mod.rs +++ b/pallas-network/src/miniprotocols/chainsync/mod.rs @@ -2,8 +2,10 @@ mod buffer; mod client; mod codec; mod protocol; +mod server; pub use buffer::*; pub use client::*; pub use codec::*; pub use protocol::*; +pub use server::*; diff --git a/pallas-network/src/miniprotocols/chainsync/protocol.rs b/pallas-network/src/miniprotocols/chainsync/protocol.rs index ee1eef57..1a9015c4 100644 --- a/pallas-network/src/miniprotocols/chainsync/protocol.rs +++ b/pallas-network/src/miniprotocols/chainsync/protocol.rs @@ -5,6 +5,8 @@ use crate::miniprotocols::Point; #[derive(Debug, Clone)] pub struct Tip(pub Point, pub u64); +pub type IntersectResponse = (Option, Tip); + #[derive(Debug, PartialEq, Eq, Clone)] pub enum State { Idle, diff --git a/pallas-network/src/miniprotocols/chainsync/server.rs b/pallas-network/src/miniprotocols/chainsync/server.rs new file mode 100644 index 00000000..1c137b3f --- /dev/null +++ b/pallas-network/src/miniprotocols/chainsync/server.rs @@ -0,0 +1,292 @@ +use pallas_codec::Fragment; +use std::marker::PhantomData; +use thiserror::Error; +use tracing::debug; + +use crate::miniprotocols::Point; +use crate::multiplexer; + +use super::{BlockContent, HeaderContent, Message, State, Tip}; + +#[derive(Error, Debug)] +pub enum ServerError { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("inbound message is not valid for current state")] + InvalidInbound, + + #[error("outbound message is not valid for current state")] + InvalidOutbound, + + #[error("error while sending or receiving data through the channel")] + Plexer(multiplexer::Error), +} + +#[derive(Debug)] +pub enum ClientRequest { + Intersect(Vec), + RequestNext, +} + +pub struct Server(State, multiplexer::ChannelBuffer, PhantomData) +where + Message: Fragment; + +impl Server +where + Message: Fragment, +{ + /// Constructs a new ChainSync `Server` instance. + /// + /// # Arguments + /// + /// * `channel` - An instance of `multiplexer::AgentChannel` to be used for + /// communication. + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self( + State::Idle, + multiplexer::ChannelBuffer::new(channel), + PhantomData {}, + ) + } + + /// Returns the current state of the server. + pub fn state(&self) -> &State { + &self.0 + } + + /// Checks if the server state is done. + pub fn is_done(&self) -> bool { + self.0 == State::Done + } + + /// Checks if the server has agency. + pub fn has_agency(&self) -> bool { + match self.state() { + State::Idle => false, + State::CanAwait => true, + State::MustReply => true, + State::Intersect => true, + State::Done => false, + } + } + + fn assert_agency_is_ours(&self) -> Result<(), ServerError> { + if !self.has_agency() { + Err(ServerError::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), ServerError> { + if self.has_agency() { + Err(ServerError::AgencyIsOurs) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ServerError> { + match (&self.0, msg) { + (State::CanAwait, Message::RollForward(_, _)) => Ok(()), + (State::CanAwait, Message::RollBackward(_, _)) => Ok(()), + (State::CanAwait, Message::AwaitReply) => Ok(()), + (State::MustReply, Message::RollForward(_, _)) => Ok(()), + (State::MustReply, Message::RollBackward(_, _)) => Ok(()), + (State::Intersect, Message::IntersectFound(_, _)) => Ok(()), + (State::Intersect, Message::IntersectNotFound(_)) => Ok(()), + _ => Err(ServerError::InvalidOutbound), + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ServerError> { + match (&self.0, msg) { + (State::Idle, Message::RequestNext) => Ok(()), + (State::Idle, Message::FindIntersect(_)) => Ok(()), + (State::Idle, Message::Done) => Ok(()), + _ => Err(ServerError::InvalidInbound), + } + } + + /// Sends a message to the client + /// + /// # Arguments + /// + /// * `msg` - A reference to the `Message` to be sent. + /// + /// # Errors + /// + /// Returns an error if the agency is not ours or if the outbound state is + /// invalid. + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ServerError> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + + self.1 + .send_msg_chunks(msg) + .await + .map_err(ServerError::Plexer)?; + + Ok(()) + } + + /// Receives the next message from the client. + /// + /// # Errors + /// + /// Returns an error if the agency is not theirs or if the inbound state is + /// invalid. + async fn recv_message(&mut self) -> Result, ServerError> { + self.assert_agency_is_theirs()?; + + let msg = self.1.recv_full_msg().await.map_err(ServerError::Plexer)?; + + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + /// Receive a message from the client when the protocol state is Idle. + /// + /// # Errors + /// + /// Returns an error if the agency is not theirs or if the inbound message + /// is invalid for Idle protocol state. + pub async fn recv_while_idle(&mut self) -> Result, ServerError> { + match self.recv_message().await? { + Message::FindIntersect(points) => { + self.0 = State::Intersect; + Ok(Some(ClientRequest::Intersect(points))) + } + Message::RequestNext => { + self.0 = State::CanAwait; + Ok(Some(ClientRequest::RequestNext)) + } + Message::Done => { + self.0 = State::Done; + + Ok(None) + } + _ => Err(ServerError::InvalidInbound), + } + } + + /// Sends an IntersectNotFound message to the client. + /// + /// # Arguments + /// + /// * `tip` - the most recent point of the server's chain. + /// + /// # Errors + /// + /// Returns an error if the message cannot be sent or if it's not valid for + /// the current state of the server. + pub async fn send_intersect_not_found(&mut self, tip: Tip) -> Result<(), ServerError> { + debug!("send intersect not found"); + + let msg = Message::IntersectNotFound(tip); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + /// Sends an IntersectFound message to the client. + /// + /// # Arguments + /// + /// * `point` - the first point in the client's provided list of intersect + /// points that was found in the servers's current chain. + /// * `tip` - the most recent point of the server's chain. + /// + /// # Errors + /// + /// Returns an error if the message cannot be sent or if it's not valid for + /// the current state of the server. + pub async fn send_intersect_found( + &mut self, + point: Point, + tip: Tip, + ) -> Result<(), ServerError> { + debug!("send intersect found ({point:?}"); + + let msg = Message::IntersectFound(point, tip); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + /// Sends a RollForward message to the client. + /// + /// # Arguments + /// + /// * `content` - the data to send to the client: for example block headers + /// for N2N or full blocks for N2C. + /// * `tip` - the most recent point of the server's chain. + /// + /// # Errors + /// + /// Returns an error if the message cannot be sent or if it's not valid for + /// the current state of the server. + pub async fn send_roll_forward(&mut self, content: O, tip: Tip) -> Result<(), ServerError> { + debug!("send roll forward"); + + let msg = Message::RollForward(content, tip); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + /// Sends a RollBackward message to the client. + /// + /// # Arguments + /// + /// * `point` - point at which the client should rollback their chain to. + /// * `tip` - the most recent point of the server's chain. + /// + /// # Errors + /// + /// Returns an error if the message cannot be sent or if it's not valid for + /// the current state of the server. + pub async fn send_roll_backward(&mut self, point: Point, tip: Tip) -> Result<(), ServerError> { + debug!("send roll backward {point:?}"); + + let msg = Message::RollBackward(point, tip); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + /// Sends an AwaitReply message to the client. + /// + /// # Arguments + /// + /// * `point` - point at which the client should rollback their chain to. + /// * `tip` - the most recent point of the server's chain. + /// + /// # Errors + /// + /// Returns an error if the message cannot be sent or if it's not valid for + /// the current state of the server. + pub async fn send_await_reply(&mut self) -> Result<(), ServerError> { + debug!("send await reply"); + + let msg = Message::AwaitReply; + self.send_message(&msg).await?; + self.0 = State::MustReply; + + Ok(()) + } +} + +pub type N2NServer = Server; + +pub type N2CServer = Server; diff --git a/pallas-network/src/miniprotocols/handshake/client.rs b/pallas-network/src/miniprotocols/handshake/client.rs index 0a795ea1..9fed8c37 100644 --- a/pallas-network/src/miniprotocols/handshake/client.rs +++ b/pallas-network/src/miniprotocols/handshake/client.rs @@ -1,4 +1,5 @@ use pallas_codec::Fragment; +use std::fmt::Debug; use std::marker::PhantomData; use tracing::debug; @@ -6,16 +7,17 @@ use super::{Error, Message, RefuseReason, State, VersionNumber, VersionTable}; use crate::multiplexer; #[derive(Debug)] -pub enum Confirmation { +pub enum Confirmation { Accepted(VersionNumber, D), Rejected(RefuseReason), + QueryReply(VersionTable), } pub struct Client(State, multiplexer::ChannelBuffer, PhantomData); impl Client where - D: std::fmt::Debug + Clone, + D: Debug + Clone, Message: Fragment, { pub fn new(channel: multiplexer::AgentChannel) -> Self { @@ -69,6 +71,7 @@ where match (&self.0, msg) { (State::Confirm, Message::Accept(..)) => Ok(()), (State::Confirm, Message::Refuse(..)) => Ok(()), + (State::Confirm, Message::QueryReply(..)) => Ok(()), _ => Err(Error::InvalidInbound), } } @@ -113,6 +116,11 @@ where Ok(Confirmation::Rejected(r)) } + Message::QueryReply(version_table) => { + debug!("handshake query reply"); + + Ok(Confirmation::QueryReply(version_table)) + } _ => Err(Error::InvalidInbound), } } diff --git a/pallas-network/src/miniprotocols/handshake/n2c.rs b/pallas-network/src/miniprotocols/handshake/n2c.rs index 212998ea..e0b7bd3e 100644 --- a/pallas-network/src/miniprotocols/handshake/n2c.rs +++ b/pallas-network/src/miniprotocols/handshake/n2c.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use pallas_codec::minicbor::data::Type; use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; use super::protocol::NetworkMagic; @@ -18,22 +19,28 @@ const PROTOCOL_V9: u64 = 32777; const PROTOCOL_V10: u64 = 32778; const PROTOCOL_V11: u64 = 32779; const PROTOCOL_V12: u64 = 32780; +const PROTOCOL_V13: u64 = 32781; +const PROTOCOL_V14: u64 = 32782; +const PROTOCOL_V15: u64 = 32783; impl VersionTable { pub fn v1_and_above(network_magic: u64) -> VersionTable { let values = vec![ - (PROTOCOL_V1, VersionData(network_magic)), - (PROTOCOL_V2, VersionData(network_magic)), - (PROTOCOL_V3, VersionData(network_magic)), - (PROTOCOL_V4, VersionData(network_magic)), - (PROTOCOL_V5, VersionData(network_magic)), - (PROTOCOL_V6, VersionData(network_magic)), - (PROTOCOL_V7, VersionData(network_magic)), - (PROTOCOL_V8, VersionData(network_magic)), - (PROTOCOL_V9, VersionData(network_magic)), - (PROTOCOL_V10, VersionData(network_magic)), - (PROTOCOL_V11, VersionData(network_magic)), - (PROTOCOL_V12, VersionData(network_magic)), + (PROTOCOL_V1, VersionData(network_magic, None)), + (PROTOCOL_V2, VersionData(network_magic, None)), + (PROTOCOL_V3, VersionData(network_magic, None)), + (PROTOCOL_V4, VersionData(network_magic, None)), + (PROTOCOL_V5, VersionData(network_magic, None)), + (PROTOCOL_V6, VersionData(network_magic, None)), + (PROTOCOL_V7, VersionData(network_magic, None)), + (PROTOCOL_V8, VersionData(network_magic, None)), + (PROTOCOL_V9, VersionData(network_magic, None)), + (PROTOCOL_V10, VersionData(network_magic, None)), + (PROTOCOL_V11, VersionData(network_magic, None)), + (PROTOCOL_V12, VersionData(network_magic, None)), + (PROTOCOL_V13, VersionData(network_magic, None)), + (PROTOCOL_V14, VersionData(network_magic, None)), + (PROTOCOL_V15, VersionData(network_magic, Some(false))), ] .into_iter() .collect::>(); @@ -42,7 +49,7 @@ impl VersionTable { } pub fn only_v10(network_magic: u64) -> VersionTable { - let values = vec![(PROTOCOL_V10, VersionData(network_magic))] + let values = vec![(PROTOCOL_V10, VersionData(network_magic, None))] .into_iter() .collect::>(); @@ -51,19 +58,36 @@ impl VersionTable { pub fn v10_and_above(network_magic: u64) -> VersionTable { let values = vec![ - (PROTOCOL_V10, VersionData(network_magic)), - (PROTOCOL_V11, VersionData(network_magic)), - (PROTOCOL_V12, VersionData(network_magic)), + (PROTOCOL_V10, VersionData(network_magic, None)), + (PROTOCOL_V11, VersionData(network_magic, None)), + (PROTOCOL_V12, VersionData(network_magic, None)), + (PROTOCOL_V13, VersionData(network_magic, None)), + (PROTOCOL_V14, VersionData(network_magic, None)), + (PROTOCOL_V15, VersionData(network_magic, Some(false))), ] .into_iter() .collect::>(); VersionTable { values } } + + pub fn v15_with_query(network_magic: u64) -> VersionTable { + let values = vec![(PROTOCOL_V15, VersionData(network_magic, Some(true)))] + .into_iter() + .collect::>(); + + VersionTable { values } + } } -#[derive(Debug, Clone)] -pub struct VersionData(NetworkMagic); +#[derive(Debug, Clone, PartialEq)] +pub struct VersionData(NetworkMagic, Option); + +impl VersionData { + pub fn new(magic: NetworkMagic, param: Option) -> Self { + Self(magic, param) + } +} impl Encode<()> for VersionData { fn encode( @@ -71,7 +95,16 @@ impl Encode<()> for VersionData { e: &mut Encoder, _ctx: &mut (), ) -> Result<(), encode::Error> { - e.u64(self.0)?; + match self.1 { + None => { + e.u64(self.0)?; + } + Some(is_query) => { + e.array(2)?; + e.u64(self.0)?; + e.bool(is_query)?; + } + } Ok(()) } @@ -79,8 +112,18 @@ impl Encode<()> for VersionData { impl<'b> Decode<'b, ()> for VersionData { fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { - let network_magic = d.u64()?; - - Ok(Self(network_magic)) + match d.datatype()? { + Type::U8 | Type::U16 | Type::U32 | Type::U64 => { + let network_magic = d.u64()?; + Ok(Self(network_magic, None)) + } + Type::Array => { + d.array()?; + let network_magic = d.u64()?; + let is_query = d.bool()?; + Ok(Self(network_magic, Some(is_query))) + } + _ => Err(decode::Error::message("unknown type for VersionData")), + } } } diff --git a/pallas-network/src/miniprotocols/handshake/n2n.rs b/pallas-network/src/miniprotocols/handshake/n2n.rs index e9cad835..b5841231 100644 --- a/pallas-network/src/miniprotocols/handshake/n2n.rs +++ b/pallas-network/src/miniprotocols/handshake/n2n.rs @@ -57,7 +57,7 @@ impl VersionTable { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct VersionData { network_magic: u64, initiator_and_responder_diffusion_mode: bool, diff --git a/pallas-network/src/miniprotocols/handshake/protocol.rs b/pallas-network/src/miniprotocols/handshake/protocol.rs index 1ab2a28d..ca7b90fa 100644 --- a/pallas-network/src/miniprotocols/handshake/protocol.rs +++ b/pallas-network/src/miniprotocols/handshake/protocol.rs @@ -55,8 +55,17 @@ impl<'b, T> Decode<'b, ()> for VersionTable where T: Debug + Clone + Decode<'b, ()>, { - fn decode(d: &mut Decoder<'b>, ctx: &mut ()) -> Result { - let values = d.map_iter_with(ctx)?.collect::>()?; + fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { + let len = d.map()?.ok_or(decode::Error::message( + "expected def-length map for versiontable", + ))?; + let mut values = HashMap::new(); + + for _ in 0..len { + let key = d.u64()?; + let value = d.decode()?; + values.insert(key, value); + } Ok(VersionTable { values }) } } @@ -73,6 +82,7 @@ where Propose(VersionTable), Accept(VersionNumber, D), Refuse(RefuseReason), + QueryReply(VersionTable), } impl Encode<()> for Message @@ -100,6 +110,10 @@ where e.array(2)?.u16(2)?; e.encode(reason)?; } + Message::QueryReply(version_table) => { + e.array(2)?.u16(3)?; + e.encode(version_table)?; + } }; Ok(()) @@ -128,6 +142,10 @@ where let reason: RefuseReason = d.decode()?; Ok(Message::Refuse(reason)) } + 3 => { + let version_table = d.decode()?; + Ok(Message::QueryReply(version_table)) + } _ => Err(decode::Error::message( "unknown variant for handshake message", )), diff --git a/pallas-network/src/miniprotocols/handshake/server.rs b/pallas-network/src/miniprotocols/handshake/server.rs index cd05a5b1..c06b9ce0 100644 --- a/pallas-network/src/miniprotocols/handshake/server.rs +++ b/pallas-network/src/miniprotocols/handshake/server.rs @@ -1,6 +1,7 @@ use std::marker::PhantomData; use pallas_codec::Fragment; +use tracing::{debug, warn}; use super::{Error, Message, RefuseReason, State, VersionNumber, VersionTable}; use crate::multiplexer; @@ -9,7 +10,7 @@ pub struct Server(State, multiplexer::ChannelBuffer, PhantomData); impl Server where - D: std::fmt::Debug + Clone, + D: std::fmt::Debug + Clone + std::cmp::PartialEq, Message: Fragment, { pub fn new(channel: multiplexer::AgentChannel) -> Self { @@ -109,6 +110,70 @@ where Ok(()) } + /// Perform a handshake with the client + /// + /// Performs a full handshake with the client, where `versions` are the + /// acceptable versions supported by the server. + pub async fn handshake( + &mut self, + versions: VersionTable, + ) -> Result, Error> { + // receive proposed versions + let client_versions = self + .receive_proposed_versions() + .await? + .values + .into_iter() + .collect::>(); + + // find highest intersect with our version table (TODO: improve) + let mut versions = versions.values.into_iter().collect::>(); + + versions.sort_by(|a, b| b.0.cmp(&a.0)); + + for (ver_num, ver_data) in versions.clone() { + for (client_ver_num, client_ver_data) in client_versions.clone() { + if ver_num == client_ver_num { + if ver_data == client_ver_data { + // found a version number and extra data match + debug!("accepting hs with ({}, {:?})", ver_num, ver_data); + + self.accept_version(ver_num, ver_data.clone()).await?; + + return Ok(Some((ver_num, ver_data))); + } else { + warn!( + "rejecting hs as params not acceptable - server: {:?}, client: {:?}", + ver_data, client_ver_data + ); + + // found version number match but extra data not acceptable + self.refuse(RefuseReason::Refused( + ver_num, + "Proposed extra params don't match".into(), + )) + .await?; + + return Ok(None); + } + } + } + } + + warn!( + "rejecting hs as no version intersect found - server: {:?}, client: {:?}", + versions, client_versions + ); + + // failed to find a version number intersection + self.refuse(RefuseReason::VersionMismatch( + versions.into_iter().map(|(num, _)| num).collect(), + )) + .await?; + + Ok(None) + } + pub fn unwrap(self) -> multiplexer::AgentChannel { self.1.unwrap() } diff --git a/pallas-network/src/miniprotocols/localstate/client.rs b/pallas-network/src/miniprotocols/localstate/client.rs index ef5e2ccd..34438d86 100644 --- a/pallas-network/src/miniprotocols/localstate/client.rs +++ b/pallas-network/src/miniprotocols/localstate/client.rs @@ -1,57 +1,52 @@ +use pallas_codec::utils::AnyCbor; use std::fmt::Debug; - -use pallas_codec::Fragment; - -use std::marker::PhantomData; use thiserror::*; -use super::{AcquireFailure, Message, Query, State}; +use super::{AcquireFailure, Message, State}; use crate::miniprotocols::Point; use crate::multiplexer; #[derive(Error, Debug)] -pub enum Error { +pub enum ClientError { #[error("attempted to receive message while agency is ours")] AgencyIsOurs, + #[error("attempted to send message while agency is theirs")] AgencyIsTheirs, + #[error("inbound message is not valid for current state")] InvalidInbound, + #[error("outbound message is not valid for current state")] InvalidOutbound, + #[error("failure acquiring point, not found")] AcquirePointNotFound, + #[error("failure acquiring point, too old")] AcquirePointTooOld, + + #[error("failure decoding CBOR data")] + InvalidCbor(pallas_codec::minicbor::decode::Error), + #[error("error while sending or receiving data through the channel")] Plexer(multiplexer::Error), } -impl From for Error { +impl From for ClientError { fn from(x: AcquireFailure) -> Self { match x { - AcquireFailure::PointTooOld => Error::AcquirePointTooOld, - AcquireFailure::PointNotOnChain => Error::AcquirePointNotFound, + AcquireFailure::PointTooOld => ClientError::AcquirePointTooOld, + AcquireFailure::PointNotOnChain => ClientError::AcquirePointNotFound, } } } -pub struct Client(State, multiplexer::ChannelBuffer, PhantomData) -where - Q: Query, - Message: Fragment; +pub struct GenericClient(State, multiplexer::ChannelBuffer); -impl Client -where - Q: Query, - Message: Fragment, -{ +impl GenericClient { pub fn new(channel: multiplexer::AgentChannel) -> Self { - Self( - State::Idle, - multiplexer::ChannelBuffer::new(channel), - PhantomData {}, - ) + Self(State::Idle, multiplexer::ChannelBuffer::new(channel)) } pub fn state(&self) -> &State { @@ -71,66 +66,94 @@ where } } - fn assert_agency_is_ours(&self) -> Result<(), Error> { + fn assert_agency_is_ours(&self) -> Result<(), ClientError> { if !self.has_agency() { - Err(Error::AgencyIsTheirs) + Err(ClientError::AgencyIsTheirs) } else { Ok(()) } } - fn assert_agency_is_theirs(&self) -> Result<(), Error> { + fn assert_agency_is_theirs(&self) -> Result<(), ClientError> { if self.has_agency() { - Err(Error::AgencyIsOurs) + Err(ClientError::AgencyIsOurs) } else { Ok(()) } } - fn assert_outbound_state(&self, msg: &Message) -> Result<(), Error> { + fn assert_outbound_state(&self, msg: &Message) -> Result<(), ClientError> { match (&self.0, msg) { (State::Idle, Message::Acquire(_)) => Ok(()), (State::Idle, Message::Done) => Ok(()), (State::Acquired, Message::Query(_)) => Ok(()), + (State::Acquired, Message::ReAcquire(_)) => Ok(()), (State::Acquired, Message::Release) => Ok(()), - _ => Err(Error::InvalidOutbound), + _ => Err(ClientError::InvalidOutbound), } } - fn assert_inbound_state(&self, msg: &Message) -> Result<(), Error> { + fn assert_inbound_state(&self, msg: &Message) -> Result<(), ClientError> { match (&self.0, msg) { (State::Acquiring, Message::Acquired) => Ok(()), (State::Acquiring, Message::Failure(_)) => Ok(()), (State::Querying, Message::Result(_)) => Ok(()), - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } - pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { + pub async fn send_message(&mut self, msg: &Message) -> Result<(), ClientError> { self.assert_agency_is_ours()?; self.assert_outbound_state(msg)?; - self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; + self.1 + .send_msg_chunks(msg) + .await + .map_err(ClientError::Plexer)?; Ok(()) } - pub async fn recv_message(&mut self) -> Result, Error> { + pub async fn recv_message(&mut self) -> Result { self.assert_agency_is_theirs()?; - let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; + let msg = self.1.recv_full_msg().await.map_err(ClientError::Plexer)?; self.assert_inbound_state(&msg)?; Ok(msg) } - pub async fn send_acquire(&mut self, point: Option) -> Result<(), Error> { - let msg = Message::::Acquire(point); + pub async fn send_acquire(&mut self, point: Option) -> Result<(), ClientError> { + let msg = Message::Acquire(point); + self.send_message(&msg).await?; + self.0 = State::Acquiring; + + Ok(()) + } + + pub async fn send_reacquire(&mut self, point: Option) -> Result<(), ClientError> { + let msg = Message::ReAcquire(point); self.send_message(&msg).await?; self.0 = State::Acquiring; Ok(()) } - pub async fn recv_while_acquiring(&mut self) -> Result<(), Error> { + pub async fn send_release(&mut self) -> Result<(), ClientError> { + let msg = Message::Release; + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_done(&mut self) -> Result<(), ClientError> { + let msg = Message::Done; + self.send_message(&msg).await?; + self.0 = State::Done; + + Ok(()) + } + + pub async fn recv_while_acquiring(&mut self) -> Result<(), ClientError> { match self.recv_message().await? { Message::Acquired => { self.0 = State::Acquired; @@ -138,39 +161,49 @@ where } Message::Failure(x) => { self.0 = State::Idle; - Err(Error::from(x)) + Err(ClientError::from(x)) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } - pub async fn acquire(&mut self, point: Option) -> Result<(), Error> { + pub async fn acquire(&mut self, point: Option) -> Result<(), ClientError> { self.send_acquire(point).await?; self.recv_while_acquiring().await } - pub async fn send_query(&mut self, request: Q::Request) -> Result<(), Error> { - let msg = Message::::Query(request); + pub async fn send_query(&mut self, request: AnyCbor) -> Result { + let msg = Message::Query(request); self.send_message(&msg).await?; self.0 = State::Querying; - Ok(()) + Ok(msg) } - pub async fn recv_while_querying(&mut self) -> Result { + pub async fn recv_while_querying(&mut self) -> Result { match self.recv_message().await? { - Message::Result(x) => { + Message::Result(result) => { self.0 = State::Acquired; - Ok(x) + Ok(result) } - _ => Err(Error::InvalidInbound), + _ => Err(ClientError::InvalidInbound), } } - pub async fn query(&mut self, request: Q::Request) -> Result { + pub async fn query_any(&mut self, request: AnyCbor) -> Result { self.send_query(request).await?; self.recv_while_querying().await } + + pub async fn query(&mut self, request: Q) -> Result + where + Q: pallas_codec::minicbor::Encode<()>, + for<'b> R: pallas_codec::minicbor::Decode<'b, ()>, + { + let request = AnyCbor::from_encode(request); + let response = self.query_any(request).await?; + response.into_decode().map_err(ClientError::InvalidCbor) + } } -pub type ClientV10 = Client; +pub type Client = GenericClient; diff --git a/pallas-network/src/miniprotocols/localstate/codec.rs b/pallas-network/src/miniprotocols/localstate/codec.rs index bf82b2bf..7673004f 100644 --- a/pallas-network/src/miniprotocols/localstate/codec.rs +++ b/pallas-network/src/miniprotocols/localstate/codec.rs @@ -1,6 +1,6 @@ use pallas_codec::minicbor::{decode, encode, Decode, Encode, Encoder}; -use super::{AcquireFailure, Message, Query}; +use super::{AcquireFailure, Message}; impl Encode<()> for AcquireFailure { fn encode( @@ -36,12 +36,7 @@ impl<'b> Decode<'b, ()> for AcquireFailure { } } -impl Encode<()> for Message -where - Q: Query, - Q::Request: Encode<()>, - Q::Response: Encode<()>, -{ +impl Encode<()> for Message { fn encode( &self, e: &mut Encoder, @@ -68,13 +63,11 @@ where } Message::Query(query) => { e.array(2)?.u16(3)?; - e.array(1)?; e.encode(query)?; Ok(()) } Message::Result(result) => { e.array(2)?.u16(4)?; - e.array(1)?; e.encode(result)?; Ok(()) } @@ -99,12 +92,7 @@ where } } -impl<'b, Q> Decode<'b, ()> for Message -where - Q: Query, - Q::Request: Decode<'b, ()>, - Q::Response: Decode<'b, ()>, -{ +impl<'b> Decode<'b, ()> for Message { fn decode( d: &mut pallas_codec::minicbor::Decoder<'b>, _ctx: &mut (), diff --git a/pallas-network/src/miniprotocols/localstate/mod.rs b/pallas-network/src/miniprotocols/localstate/mod.rs index c327f1c9..2485b17b 100644 --- a/pallas-network/src/miniprotocols/localstate/mod.rs +++ b/pallas-network/src/miniprotocols/localstate/mod.rs @@ -1,8 +1,11 @@ mod client; mod codec; mod protocol; -pub mod queries; +mod server; + +pub mod queries_v16; pub use client::*; pub use codec::*; pub use protocol::*; +pub use server::*; diff --git a/pallas-network/src/miniprotocols/localstate/protocol.rs b/pallas-network/src/miniprotocols/localstate/protocol.rs index 1c82106d..1d162c27 100644 --- a/pallas-network/src/miniprotocols/localstate/protocol.rs +++ b/pallas-network/src/miniprotocols/localstate/protocol.rs @@ -1,5 +1,7 @@ use std::fmt::Debug; +use pallas_codec::utils::AnyCbor; + use crate::miniprotocols::Point; #[derive(Debug, PartialEq, Eq, Clone)] @@ -17,18 +19,13 @@ pub enum AcquireFailure { PointNotOnChain, } -pub trait Query: Debug { - type Request: Clone + Debug; - type Response: Clone + Debug; -} - #[derive(Debug)] -pub enum Message { +pub enum Message { Acquire(Option), Failure(AcquireFailure), Acquired, - Query(Q::Request), - Result(Q::Response), + Query(AnyCbor), + Result(AnyCbor), ReAcquire(Option), Release, Done, diff --git a/pallas-network/src/miniprotocols/localstate/queries.rs b/pallas-network/src/miniprotocols/localstate/queries.rs deleted file mode 100644 index e766efd6..00000000 --- a/pallas-network/src/miniprotocols/localstate/queries.rs +++ /dev/null @@ -1,78 +0,0 @@ -use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; - -use super::Query; - -#[derive(Debug, Clone)] -pub struct BlockQuery {} - -#[derive(Debug, Clone)] -pub enum RequestV10 { - BlockQuery(BlockQuery), - GetSystemStart, - GetChainBlockNo, - GetChainPoint, -} - -impl Encode<()> for RequestV10 { - fn encode( - &self, - e: &mut Encoder, - _ctx: &mut (), - ) -> Result<(), encode::Error> { - match self { - Self::BlockQuery(..) => { - todo!() - } - Self::GetSystemStart => { - e.u16(1)?; - Ok(()) - } - Self::GetChainBlockNo => { - e.u16(2)?; - Ok(()) - } - Self::GetChainPoint => { - e.u16(3)?; - Ok(()) - } - } - } -} - -impl<'b> Decode<'b, ()> for RequestV10 { - fn decode(_d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { - todo!() - } -} - -#[derive(Debug, Clone)] -pub struct GenericResponse(Vec); - -impl Encode<()> for GenericResponse { - fn encode( - &self, - _e: &mut Encoder, - _ctx: &mut (), - ) -> Result<(), encode::Error> { - todo!() - } -} - -impl<'b> Decode<'b, ()> for GenericResponse { - fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { - let start = d.position(); - d.skip()?; - let end = d.position(); - let slice = &d.input()[start..end]; - let vec = slice.to_vec(); - Ok(GenericResponse(vec)) - } -} - -#[derive(Debug, Clone)] -pub struct QueryV10 {} - -impl Query for QueryV10 { - type Request = RequestV10; - type Response = GenericResponse; -} diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs new file mode 100644 index 00000000..f5be36b0 --- /dev/null +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/codec.rs @@ -0,0 +1,248 @@ +use super::*; +use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; + +impl Encode<()> for BlockQuery { + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + match self { + BlockQuery::GetLedgerTip => { + e.array(1)?; + e.u16(0)?; + } + BlockQuery::GetEpochNo => { + e.array(1)?; + e.u16(1)?; + } + BlockQuery::GetNonMyopicMemberRewards(x) => { + e.array(2)?; + e.u16(2)?; + e.encode(x)?; + } + BlockQuery::GetCurrentPParams => { + e.array(1)?; + e.u16(3)?; + } + BlockQuery::GetProposedPParamsUpdates => { + e.array(1)?; + e.u16(4)?; + } + BlockQuery::GetStakeDistribution => { + e.array(1)?; + e.u16(5)?; + } + BlockQuery::GetUTxOByAddress(x) => { + e.array(2)?; + e.u16(6)?; + e.encode(x)?; + } + BlockQuery::GetUTxOWhole => { + e.encode((7,))?; + } + BlockQuery::DebugEpochState => { + e.array(1)?; + e.u16(8)?; + } + BlockQuery::GetCBOR(x) => { + e.array(2)?; + e.u16(9)?; + e.encode(x)?; + } + BlockQuery::GetFilteredDelegationsAndRewardAccounts(x) => { + e.array(2)?; + e.u16(10)?; + e.encode(x)?; + } + BlockQuery::GetGenesisConfig => { + e.array(1)?; + e.u16(11)?; + } + BlockQuery::DebugNewEpochState => { + e.array(1)?; + e.u16(12)?; + } + BlockQuery::DebugChainDepState => { + e.array(1)?; + e.u16(13)?; + } + BlockQuery::GetRewardProvenance => { + e.array(1)?; + e.u16(14)?; + } + BlockQuery::GetUTxOByTxIn(_) => { + e.array(2)?; + e.u16(15)?; + e.encode(2)?; + } + BlockQuery::GetStakePools => { + e.array(1)?; + e.u16(16)?; + } + BlockQuery::GetStakePoolParams(x) => { + e.array(2)?; + e.u16(17)?; + e.encode(x)?; + } + BlockQuery::GetRewardInfoPools => { + e.array(1)?; + e.u16(18)?; + } + BlockQuery::GetPoolState(x) => { + e.array(2)?; + e.u16(19)?; + e.encode(x)?; + } + BlockQuery::GetStakeSnapshots(x) => { + e.array(2)?; + e.u16(20)?; + e.encode(x)?; + } + BlockQuery::GetPoolDistr(x) => { + e.array(2)?; + e.u16(21)?; + e.encode(x)?; + } + BlockQuery::GetStakeDelegDeposits(x) => { + e.array(2)?; + e.u16(22)?; + e.encode(x)?; + } + BlockQuery::GetConstitutionHash => { + e.array(1)?; + e.u16(23)?; + } + } + Ok(()) + } +} + +impl<'b> Decode<'b, ()> for BlockQuery { + fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { + d.array()?; + + match d.u16()? { + 0 => Ok(Self::GetLedgerTip), + 1 => Ok(Self::GetEpochNo), + // 2 => Ok(Self::GetNonMyopicMemberRewards(())), + 3 => Ok(Self::GetCurrentPParams), + 4 => Ok(Self::GetProposedPParamsUpdates), + 5 => Ok(Self::GetStakeDistribution), + // 6 => Ok(Self::GetUTxOByAddress(())), + // 7 => Ok(Self::GetUTxOWhole), + // 8 => Ok(Self::DebugEpochState), + // 9 => Ok(Self::GetCBOR(())), + // 10 => Ok(Self::GetFilteredDelegationsAndRewardAccounts(())), + 11 => Ok(Self::GetGenesisConfig), + // 12 => Ok(Self::DebugNewEpochState), + 13 => Ok(Self::DebugChainDepState), + 14 => Ok(Self::GetRewardProvenance), + // 15 => Ok(Self::GetUTxOByTxIn(())), + 16 => Ok(Self::GetStakePools), + // 17 => Ok(Self::GetStakePoolParams(())), + 18 => Ok(Self::GetRewardInfoPools), + // 19 => Ok(Self::GetPoolState(())), + // 20 => Ok(Self::GetStakeSnapshots(())), + // 21 => Ok(Self::GetPoolDistr(())), + // 22 => Ok(Self::GetStakeDelegDeposits(())), + // 23 => Ok(Self::GetConstitutionHash), + _ => unreachable!(), + } + } +} + +impl Encode<()> for HardForkQuery { + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + match self { + HardForkQuery::GetInterpreter => { + e.encode((0,))?; + } + HardForkQuery::GetCurrentEra => { + e.encode((1,))?; + } + } + + Ok(()) + } +} + +impl<'b> Decode<'b, ()> for HardForkQuery { + fn decode(_d: &mut Decoder<'b>, _: &mut ()) -> Result { + todo!() + } +} + +impl Encode<()> for LedgerQuery { + fn encode( + &self, + e: &mut Encoder, + _: &mut (), + ) -> Result<(), encode::Error> { + match self { + LedgerQuery::BlockQuery(era, q) => { + e.encode((0, (era, q)))?; + } + LedgerQuery::HardForkQuery(q) => { + e.encode((2, q))?; + } + } + + Ok(()) + } +} + +impl<'b> Decode<'b, ()> for LedgerQuery { + fn decode(_d: &mut Decoder<'b>, _: &mut ()) -> Result { + todo!() + } +} + +impl Encode<()> for Request { + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + match self { + Self::LedgerQuery(q) => { + e.encode((0, q))?; + Ok(()) + } + Self::GetSystemStart => { + e.array(1)?; + e.u16(1)?; + Ok(()) + } + Self::GetChainBlockNo => { + e.array(1)?; + e.u16(2)?; + Ok(()) + } + Self::GetChainPoint => { + e.array(1)?; + e.u16(3)?; + Ok(()) + } + } + } +} + +impl<'b> Decode<'b, ()> for Request { + fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { + d.array()?; + let tag = d.u16()?; + + match tag { + 0 => Ok(Self::LedgerQuery(d.decode()?)), + 1 => Ok(Self::GetSystemStart), + 2 => Ok(Self::GetChainBlockNo), + 3 => Ok(Self::GetChainPoint), + _ => Err(decode::Error::message("invalid tag")), + } + } +} diff --git a/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs new file mode 100644 index 00000000..68f3dd64 --- /dev/null +++ b/pallas-network/src/miniprotocols/localstate/queries_v16/mod.rs @@ -0,0 +1,113 @@ +// TODO: this should move to pallas::ledger crate at some point + +// required for derive attrs to work +use pallas_codec::minicbor; + +use pallas_codec::{ + minicbor::{Decode, Encode}, + utils::AnyCbor, +}; + +use crate::miniprotocols::Point; + +use super::{Client, ClientError}; + +mod codec; + +// https://github.com/input-output-hk/ouroboros-consensus/blob/main/ouroboros-consensus-cardano/src/shelley/Ouroboros/Consensus/Shelley/Ledger/Query.hs +#[derive(Debug, Clone, PartialEq)] +#[repr(u16)] +pub enum BlockQuery { + GetLedgerTip, + GetEpochNo, + GetNonMyopicMemberRewards(AnyCbor), + GetCurrentPParams, + GetProposedPParamsUpdates, + GetStakeDistribution, + GetUTxOByAddress(AnyCbor), + GetUTxOWhole, + DebugEpochState, + GetCBOR(AnyCbor), + GetFilteredDelegationsAndRewardAccounts(AnyCbor), + GetGenesisConfig, + DebugNewEpochState, + DebugChainDepState, + GetRewardProvenance, + GetUTxOByTxIn(AnyCbor), + GetStakePools, + GetStakePoolParams(AnyCbor), + GetRewardInfoPools, + GetPoolState(AnyCbor), + GetStakeSnapshots(AnyCbor), + GetPoolDistr(AnyCbor), + GetStakeDelegDeposits(AnyCbor), + GetConstitutionHash, +} + +#[derive(Debug, Clone, PartialEq)] +#[repr(u16)] +pub enum HardForkQuery { + GetInterpreter, + GetCurrentEra, +} + +pub type Proto = u16; +pub type Era = u16; + +#[derive(Debug, Clone, PartialEq)] +pub enum LedgerQuery { + BlockQuery(Era, BlockQuery), + HardForkQuery(HardForkQuery), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Request { + LedgerQuery(LedgerQuery), + GetSystemStart, + GetChainBlockNo, + GetChainPoint, +} + +#[derive(Debug, Encode, Decode, PartialEq)] +pub struct SystemStart { + #[n(0)] + pub year: u32, + + #[n(1)] + pub day_of_year: u32, + + #[n(2)] + pub picoseconds_of_day: u64, +} + +pub async fn get_chain_point(client: &mut Client) -> Result { + let query = Request::GetChainPoint; + let result = client.query(query).await?; + + Ok(result) +} + +pub async fn get_current_era(client: &mut Client) -> Result { + let query = HardForkQuery::GetCurrentEra; + let query = LedgerQuery::HardForkQuery(query); + let query = Request::LedgerQuery(query); + let result = client.query(query).await?; + + Ok(result) +} + +pub async fn get_system_start(client: &mut Client) -> Result { + let query = Request::GetSystemStart; + let result = client.query(query).await?; + + Ok(result) +} + +pub async fn get_block_epoch_number(client: &mut Client, era: u16) -> Result { + let query = BlockQuery::GetEpochNo; + let query = LedgerQuery::BlockQuery(era, query); + let query = Request::LedgerQuery(query); + let (result,): (_,) = client.query(query).await?; + + Ok(result) +} diff --git a/pallas-network/src/miniprotocols/localstate/server.rs b/pallas-network/src/miniprotocols/localstate/server.rs new file mode 100644 index 00000000..32f7fe83 --- /dev/null +++ b/pallas-network/src/miniprotocols/localstate/server.rs @@ -0,0 +1,166 @@ +use pallas_codec::utils::AnyCbor; +use std::fmt::Debug; +use thiserror::*; + +use super::{AcquireFailure, Message, State}; +use crate::miniprotocols::Point; +use crate::multiplexer; + +#[derive(Error, Debug)] +pub enum Error { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + #[error("inbound message is not valid for current state")] + InvalidInbound, + #[error("outbound message is not valid for current state")] + InvalidOutbound, + #[error("error while sending or receiving data through the channel")] + Plexer(multiplexer::Error), +} + +/// Request received from the client to acquire the ledger +pub struct ClientAcquireRequest(pub Option); + +/// Request received from the client when in the Acquired state +#[derive(Debug)] +pub enum ClientQueryRequest { + ReAcquire(Option), + Query(AnyCbor), + Release, +} + +pub struct GenericServer(State, multiplexer::ChannelBuffer); + +impl GenericServer { + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self(State::Idle, multiplexer::ChannelBuffer::new(channel)) + } + + pub fn state(&self) -> &State { + &self.0 + } + + pub fn is_done(&self) -> bool { + self.0 == State::Done + } + + fn has_agency(&self) -> bool { + matches!(self.state(), State::Acquiring | State::Querying) + } + + fn assert_agency_is_ours(&self) -> Result<(), Error> { + if !self.has_agency() { + Err(Error::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), Error> { + if self.has_agency() { + Err(Error::AgencyIsOurs) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), Error> { + match (&self.0, msg) { + (State::Acquiring, Message::Acquired) => Ok(()), + (State::Acquiring, Message::Failure(_)) => Ok(()), + (State::Querying, Message::Result(_)) => Ok(()), + _ => Err(Error::InvalidOutbound), + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), Error> { + match (&self.0, msg) { + (State::Idle, Message::Acquire(_)) => Ok(()), + (State::Idle, Message::Done) => Ok(()), + (State::Acquired, Message::Query(_)) => Ok(()), + (State::Acquired, Message::ReAcquire(_)) => Ok(()), + (State::Acquired, Message::Release) => Ok(()), + _ => Err(Error::InvalidInbound), + } + } + + pub async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + self.1.send_msg_chunks(msg).await.map_err(Error::Plexer)?; + + Ok(()) + } + + pub async fn recv_message(&mut self) -> Result { + self.assert_agency_is_theirs()?; + let msg = self.1.recv_full_msg().await.map_err(Error::Plexer)?; + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + pub async fn send_failure(&mut self, reason: AcquireFailure) -> Result<(), Error> { + let msg = Message::Failure(reason); + self.send_message(&msg).await?; + self.0 = State::Idle; + + Ok(()) + } + + pub async fn send_acquired(&mut self) -> Result<(), Error> { + let msg = Message::Acquired; + self.send_message(&msg).await?; + self.0 = State::Acquired; + + Ok(()) + } + + pub async fn send_result(&mut self, response: AnyCbor) -> Result<(), Error> { + let msg = Message::Result(response); + self.send_message(&msg).await?; + self.0 = State::Acquired; + + Ok(()) + } + + /// Receive a message from the Client when the protocol is in the Idle state + /// + /// Returns the client's request to acquire the ledger or None if a Done + /// message was received from the client causing the protocol to finish. + pub async fn recv_while_idle(&mut self) -> Result, Error> { + match self.recv_message().await? { + Message::Acquire(point) => { + self.0 = State::Acquiring; + Ok(Some(ClientAcquireRequest(point))) + } + Message::Done => { + self.0 = State::Done; + Ok(None) + } + _ => Err(Error::InvalidInbound), + } + } + + pub async fn recv_while_acquired(&mut self) -> Result { + match self.recv_message().await? { + Message::ReAcquire(point) => { + self.0 = State::Acquiring; + Ok(ClientQueryRequest::ReAcquire(point)) + } + Message::Query(query) => { + self.0 = State::Querying; + Ok(ClientQueryRequest::Query(query)) + } + Message::Release => { + self.0 = State::Idle; + Ok(ClientQueryRequest::Release) + } + _ => Err(Error::InvalidInbound), + } + } +} + +pub type Server = GenericServer; diff --git a/pallas-network/src/miniprotocols/localtxsubmission/client.rs b/pallas-network/src/miniprotocols/localtxsubmission/client.rs new file mode 100644 index 00000000..c6e5e157 --- /dev/null +++ b/pallas-network/src/miniprotocols/localtxsubmission/client.rs @@ -0,0 +1,208 @@ +use std::marker::PhantomData; + +use thiserror::Error; +use tracing::debug; + +use pallas_codec::Fragment; + +use crate::miniprotocols::localtxsubmission::{EraTx, Message, RejectReason, State}; +use crate::multiplexer; + +/// Cardano specific instantiation of LocalTxSubmission client. +pub type Client = GenericClient; + +/// A generic Ouroboros client for submitting a generic transaction +/// to a server, which possibly results in a generic rejection. +pub struct GenericClient { + state: State, + muxer: multiplexer::ChannelBuffer, + pd_tx: PhantomData, + pd_reject: PhantomData, +} + +impl GenericClient +where + Message: Fragment, +{ + /// Constructs a new LocalTxSubmission `Client` instance. + /// + /// # Arguments + /// * `channel` - An instance of `multiplexer::AgentChannel` to be used for + /// communication. + pub fn new(channel: multiplexer::AgentChannel) -> Self { + Self { + state: State::Idle, + muxer: multiplexer::ChannelBuffer::new(channel), + pd_tx: Default::default(), + pd_reject: Default::default(), + } + } + + /// Submits the given `tx` to the server. + /// + /// # Arguments + /// * `tx` - transaction to submit. + /// + /// # Errors + /// Returns an error if the agency is not ours or if the outbound state is + /// invalid. + pub async fn submit_tx(&mut self, tx: Tx) -> Result<(), Error> { + self.send_submit_tx(tx).await?; + self.recv_submit_tx_response().await + } + + /// Terminates the protocol gracefully. + /// + /// # Errors + /// Returns an error if the agency is not ours or if the outbound state is + /// invalid. + pub async fn terminate_gracefully(&mut self) -> Result<(), Error> { + let msg = Message::Done; + self.send_message(&msg).await?; + self.state = State::Done; + + Ok(()) + } + + /// Returns the current state of the client. + fn state(&self) -> &State { + &self.state + } + + /// Checks if the client has agency. + fn has_agency(&self) -> bool { + match self.state() { + State::Idle => true, + State::Busy | State::Done => false, + } + } + + fn assert_agency_is_ours(&self) -> Result<(), Error> { + if !self.has_agency() { + Err(Error::AgencyIsTheirs) + } else { + Ok(()) + } + } + + fn assert_agency_is_theirs(&self) -> Result<(), Error> { + if self.has_agency() { + Err(Error::AgencyIsOurs) + } else { + Ok(()) + } + } + + fn assert_outbound_state(&self, msg: &Message) -> Result<(), Error> { + match (&self.state, msg) { + (State::Idle, Message::SubmitTx(_) | Message::Done) => Ok(()), + _ => Err(Error::InvalidOutbound), + } + } + + fn assert_inbound_state(&self, msg: &Message) -> Result<(), Error> { + match (&self.state, msg) { + (State::Busy, Message::AcceptTx | Message::RejectTx(_)) => Ok(()), + _ => Err(Error::InvalidInbound), + } + } + + /// Sends a message to the server + /// + /// # Arguments + /// + /// * `msg` - A reference to the `Message` to be sent. + /// + /// # Errors + /// Returns an error if the agency is not ours or if the outbound state is + /// invalid. + async fn send_message(&mut self, msg: &Message) -> Result<(), Error> { + self.assert_agency_is_ours()?; + self.assert_outbound_state(msg)?; + + self.muxer + .send_msg_chunks(msg) + .await + .map_err(Error::ChannelError)?; + + Ok(()) + } + + /// Receives the next message from the server. + /// + /// # Errors + /// Returns an error if the agency is not theirs or if the inbound state is + /// invalid. + async fn recv_message(&mut self) -> Result, Error> { + self.assert_agency_is_theirs()?; + + let msg = self + .muxer + .recv_full_msg() + .await + .map_err(Error::ChannelError)?; + + self.assert_inbound_state(&msg)?; + + Ok(msg) + } + + /// Sends SubmitTx message to the server. + /// + /// # Arguments + /// * `tx` - transaction to submit. + /// + /// # Errors + /// Returns an error if the agency is not ours or if the outbound state is + /// invalid. + async fn send_submit_tx(&mut self, tx: Tx) -> Result<(), Error> { + let msg = Message::SubmitTx(tx); + self.send_message(&msg).await?; + self.state = State::Busy; + + debug!("sent SubmitTx"); + + Ok(()) + } + + /// Receives SubmitTx response from the server. + /// + /// # Errors + /// Returns an error if the inbound message is invalid. + async fn recv_submit_tx_response(&mut self) -> Result<(), Error> { + debug!("waiting for SubmitTx response"); + + match self.recv_message().await? { + Message::AcceptTx => { + self.state = State::Idle; + Ok(()) + } + Message::RejectTx(rejection) => { + self.state = State::Idle; + Err(Error::TxRejected(rejection)) + } + _ => Err(Error::InvalidInbound), + } + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("attempted to receive message while agency is ours")] + AgencyIsOurs, + + #[error("attempted to send message while agency is theirs")] + AgencyIsTheirs, + + #[error("inbound message is not valid for current state")] + InvalidInbound, + + #[error("outbound message is not valid for current state")] + InvalidOutbound, + + #[error("error while sending or receiving data through the channel")] + ChannelError(multiplexer::Error), + + #[error("tx was rejected by the server")] + TxRejected(Reject), +} diff --git a/pallas-network/src/miniprotocols/localtxsubmission/codec.rs b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs new file mode 100644 index 00000000..ce54a8a4 --- /dev/null +++ b/pallas-network/src/miniprotocols/localtxsubmission/codec.rs @@ -0,0 +1,152 @@ +use pallas_codec::minicbor::data::Tag; +use pallas_codec::minicbor::{decode, encode, Decode, Decoder, Encode, Encoder}; + +use crate::miniprotocols::localtxsubmission::{EraTx, Message, RejectReason}; + +impl Encode<()> for Message +where + Tx: Encode<()>, + Reject: Encode<()>, +{ + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + match self { + Message::SubmitTx(tx) => { + e.array(2)?.u16(0)?; + e.encode(tx)?; + Ok(()) + } + Message::AcceptTx => { + e.array(1)?.u16(1)?; + Ok(()) + } + Message::RejectTx(rejection) => { + e.array(2)?.u16(2)?; + e.encode(rejection)?; + Ok(()) + } + Message::Done => { + e.array(1)?.u16(3)?; + Ok(()) + } + } + } +} + +impl<'b, Tx: Decode<'b, ()>, Reject: Decode<'b, ()>> Decode<'b, ()> for Message { + fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { + d.array()?; + let label = d.u16()?; + match label { + 0 => { + let tx = d.decode()?; + Ok(Message::SubmitTx(tx)) + } + 1 => Ok(Message::AcceptTx), + 2 => { + let rejection = d.decode()?; + Ok(Message::RejectTx(rejection)) + } + 3 => Ok(Message::Done), + _ => Err(decode::Error::message("can't decode Message")), + } + } +} + +impl<'b> Decode<'b, ()> for EraTx { + fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { + d.array()?; + let era = d.u16()?; + let tag = d.tag()?; + if tag != Tag::Cbor { + return Err(decode::Error::message("Expected encoded CBOR data item")); + } + Ok(EraTx(era, d.bytes()?.to_vec())) + } +} + +impl Encode<()> for EraTx { + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + e.array(2)?; + e.u16(self.0)?; + e.tag(Tag::Cbor)?; + e.bytes(&self.1)?; + Ok(()) + } +} + +impl<'b> Decode<'b, ()> for RejectReason { + fn decode(d: &mut Decoder<'b>, _ctx: &mut ()) -> Result { + let remainder = d.input().to_vec(); + Ok(RejectReason(remainder)) + } +} + +impl Encode<()> for RejectReason { + fn encode( + &self, + e: &mut Encoder, + _ctx: &mut (), + ) -> Result<(), encode::Error> { + e.writer_mut() + .write_all(&self.0) + .map_err(encode::Error::write)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use pallas_codec::{minicbor, Fragment}; + + use crate::miniprotocols::localtxsubmission::{EraTx, Message, RejectReason}; + use crate::multiplexer::Error; + + #[test] + fn decode_reject_message() { + let mut bytes = hex::decode(RAW_REJECT_RESPONSE).unwrap(); + let msg_res = try_decode_message::>(&mut bytes); + assert!(msg_res.is_ok()) + } + + fn try_decode_message(buffer: &mut Vec) -> Result, Error> + where + M: Fragment, + { + let mut decoder = minicbor::Decoder::new(buffer); + let maybe_msg = decoder.decode(); + + match maybe_msg { + Ok(msg) => { + let pos = decoder.position(); + buffer.drain(0..pos); + Ok(Some(msg)) + } + Err(err) if err.is_end_of_input() => Ok(None), + Err(err) => Err(Error::Decoding(err.to_string())), + } + } + + const RAW_REJECT_RESPONSE: &str = + "82028182059f820082018200820a81581c3b890fb5449baedf5342a48ee9c9ec6acbc995641be92ad21f08c686\ + 8200820183038158202628ce6ff8cc7ff0922072d930e4a693c17f991748dedece0be64819a2f9ef7782582031d\ + 54ce8d7e8cb262fc891282f44e9d24c3902dc38fac63fd469e8bf3006376b5820750852fdaf0f2dd724291ce007\ + b8e76d74bcf28076ed0c494cd90c0cfe1c9ca582008201820782000000018200820183048158201a547638b4cf4\ + a3cec386e2f898ac6bc987fadd04277e1d3c8dab5c505a5674e8158201457e4107607f83a80c3c4ffeb70910c2b\ + a3a35cf1699a2a7375f50fcc54a931820082028201830500821a00636185a2581c6f1a1f0c7ccf632cc9ff4b796\ + 87ed13ffe5b624cce288b364ebdce50a144414749581b000000032a9f8800581c795ecedb09821cb922c13060c8\ + f6377c3344fa7692551e865d86ac5da158205399c766fb7c494cddb2f7ae53cc01285474388757bc05bd575c14a\ + 713a432a901820082028201820085825820497fe6401e25733c073c01164c7f2a1a05de8c95e36580f9d1b05123\ + 70040def028258207911ba2b7d91ac56b05ea351282589fe30f4717a707a1b9defaf282afe5ba44200825820791\ + 1ba2b7d91ac56b05ea351282589fe30f4717a707a1b9defaf282afe5ba44201825820869bcb6f35e6b7912c25e5\ + cb33fb9906b097980a83f2b8ef40b51c4ef52eccd402825820efc267ad2c15c34a117535eecc877241ed836eb3e\ + 643ec90de21ca1b12fd79c20282008202820181148200820283023a000f0f6d1a004944ce820082028201830d3a\ + 000f0f6d1a00106253820082028201830182811a02409e10811a024138c01a0255e528ff"; +} diff --git a/pallas-network/src/miniprotocols/localtxsubmission/mod.rs b/pallas-network/src/miniprotocols/localtxsubmission/mod.rs new file mode 100644 index 00000000..a9d16586 --- /dev/null +++ b/pallas-network/src/miniprotocols/localtxsubmission/mod.rs @@ -0,0 +1,7 @@ +pub use client::*; +pub use codec::*; +pub use protocol::*; + +mod client; +mod codec; +mod protocol; diff --git a/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs new file mode 100644 index 00000000..3a2cc4db --- /dev/null +++ b/pallas-network/src/miniprotocols/localtxsubmission/protocol.rs @@ -0,0 +1,22 @@ +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum State { + Idle, + Busy, + Done, +} + +#[derive(Debug)] +pub enum Message { + SubmitTx(Tx), + AcceptTx, + RejectTx(Reject), + Done, +} + +// The bytes of a transaction with an era number. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct EraTx(pub u16, pub Vec); + +// Raw reject reason. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct RejectReason(pub Vec); diff --git a/pallas-network/src/miniprotocols/mod.rs b/pallas-network/src/miniprotocols/mod.rs index e261485d..6c452db2 100644 --- a/pallas-network/src/miniprotocols/mod.rs +++ b/pallas-network/src/miniprotocols/mod.rs @@ -6,6 +6,7 @@ pub mod blockfetch; pub mod chainsync; pub mod handshake; pub mod localstate; +pub mod localtxsubmission; pub mod txmonitor; pub mod txsubmission; diff --git a/pallas-network/src/miniprotocols/txmonitor/codec.rs b/pallas-network/src/miniprotocols/txmonitor/codec.rs index 4faccbfc..15da33ce 100644 --- a/pallas-network/src/miniprotocols/txmonitor/codec.rs +++ b/pallas-network/src/miniprotocols/txmonitor/codec.rs @@ -80,12 +80,13 @@ impl<'b> Decode<'b, ()> for Message { // find the specs 4 => Ok(Message::AwaitAcquire), 5 => Ok(Message::RequestNextTx), - 6 => match d.array()? { - Some(_) => { - let cbor: pallas_codec::utils::CborWrap = d.decode()?; - Ok(Message::ResponseNextTx(Some(cbor.unwrap()))) + 6 => match d.datatype()? { + pallas_codec::minicbor::data::Type::Array + | pallas_codec::minicbor::data::Type::ArrayIndef => { + let tx = d.decode()?; + Ok(Message::ResponseNextTx(Some(tx))) } - None => Ok(Message::ResponseNextTx(None)), + _ => Ok(Message::ResponseNextTx(None)), }, 7 => { let id = d.decode()?; @@ -112,3 +113,21 @@ impl<'b> Decode<'b, ()> for Message { } } } + +#[cfg(test)] +pub mod tests { + const EXAMPLE_RESPONSE_NEXT_TX_WITH_DATA: &str = "82068205d81859013184a5008282582003e4aea27ebacf5f50b10ac60cc84deba96569ce8a47fdf9199998d1fd16ec0601825820eebf8249544b7eefa7839510dfd58a7ed420f2254bd3bf632baea8cd0928b00102018182583901b98f57f569aba4cffc4d9c791f099374e9403ed5e2cb614eab25b78278b1312c2c271d260db425b8b9847ab142b395b4598d3c0b383aa696821a00924172a1581c09f2d4e4a5c3662f4c1e6a7d9600e9605279dbdcedb22d4507cb6e75a1435350461a0422bb35021a00029f3d031a063ec6470800a100818258208293ac2260e28a07657f77087d1d7ff5e3ced29ff4385abf60a9546e2bcbc04a5840d69ce3a8f9713513a9baf473c1be08fd17d1a85df2881dc107fb1f68ce02c8e7adcf1c91bce7fb58868908f7ac47310a8e97d95780beadcfd8493bebbb914d0df5f6"; + + #[test] + fn test_next_tx_response() { + let bytes = hex::decode(EXAMPLE_RESPONSE_NEXT_TX_WITH_DATA).unwrap(); + let msg: super::Message = pallas_codec::minicbor::decode(&bytes).unwrap(); + + if let super::Message::ResponseNextTx(Some((era, body))) = msg { + assert_eq!(era, 5); + assert_eq!(body.len(), 305); + } else { + unreachable!(); + } + } +} diff --git a/pallas-network/src/miniprotocols/txmonitor/protocol.rs b/pallas-network/src/miniprotocols/txmonitor/protocol.rs index 06c91bdd..43a0ebfa 100644 --- a/pallas-network/src/miniprotocols/txmonitor/protocol.rs +++ b/pallas-network/src/miniprotocols/txmonitor/protocol.rs @@ -1,6 +1,10 @@ +use pallas_codec::utils::TagWrap; + pub type Slot = u64; pub type TxId = String; -pub type Tx = Vec; +pub type Era = u8; +pub type TxBody = pallas_codec::utils::Bytes; +pub type Tx = (Era, TagWrap); #[derive(Debug, PartialEq, Eq, Clone)] pub enum State { diff --git a/pallas-network/src/multiplexer.rs b/pallas-network/src/multiplexer.rs index ec9530fd..38eef6c8 100644 --- a/pallas-network/src/multiplexer.rs +++ b/pallas-network/src/multiplexer.rs @@ -3,7 +3,6 @@ use byteorder::{ByteOrder, NetworkEndian}; use pallas_codec::{minicbor, Fragment}; use std::net::SocketAddr; -use std::path::Path; use thiserror::Error; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream, ToSocketAddrs}; @@ -12,8 +11,11 @@ use tokio::sync::mpsc::error::SendError; use tokio::time::Instant; use tracing::{debug, error, trace}; -#[cfg(not(target_os = "windows"))] -use UnixStream; +#[cfg(unix)] +use tokio::net::{UnixListener, UnixStream}; + +#[cfg(windows)] +use tokio::net::windows::named_pipe::NamedPipeClient; const HEADER_LEN: usize = 8; @@ -60,15 +62,14 @@ pub struct Segment { pub payload: Payload, } -#[cfg(target_os = "windows")] -pub enum Bearer { - Tcp(TcpStream) -} - -#[cfg(not(target_os = "windows"))] pub enum Bearer { Tcp(TcpStream), + + #[cfg(unix)] Unix(UnixStream), + + #[cfg(windows)] + NamedPipe(NamedPipeClient), } const BUFFER_LEN: usize = 1024 * 10; @@ -79,46 +80,93 @@ impl Bearer { Ok(Self::Tcp(stream)) } - pub async fn accept_tcp(listener: TcpListener) -> tokio::io::Result<(Self, SocketAddr)> { + pub async fn accept_tcp(listener: &TcpListener) -> tokio::io::Result<(Self, SocketAddr)> { let (stream, addr) = listener.accept().await?; Ok((Self::Tcp(stream), addr)) } - #[cfg(not(target_os = "windows"))] - pub async fn connect_unix(path: impl AsRef) -> Result { + #[cfg(unix)] + pub async fn connect_unix(path: impl AsRef) -> Result { let stream = UnixStream::connect(path).await?; Ok(Self::Unix(stream)) } - pub async fn readable(&self) -> tokio::io::Result<()> { + #[cfg(unix)] + pub async fn accept_unix( + listener: &UnixListener, + ) -> tokio::io::Result<(Self, tokio::net::unix::SocketAddr)> { + let (stream, addr) = listener.accept().await?; + Ok((Self::Unix(stream), addr)) + } + + #[cfg(windows)] + pub async fn connect_named_pipe( + pipe_name: impl AsRef, + ) -> Result { + // TODO: revisit if busy wait logic is required + let client = loop { + match tokio::net::windows::named_pipe::ClientOptions::new().open(&pipe_name) { + Ok(client) => break client, + Err(e) + if e.raw_os_error() + == Some(windows_sys::Win32::Foundation::ERROR_PIPE_BUSY as i32) => + { + () + } + Err(e) => return Err(e), + } + + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + }; + + Ok(Self::NamedPipe(client)) + } + + pub async fn readable(&mut self) -> tokio::io::Result<()> { match self { Bearer::Tcp(x) => x.readable().await, - #[cfg(not(target_os = "windows"))] + + #[cfg(unix)] Bearer::Unix(x) => x.readable().await, + + #[cfg(windows)] + Bearer::NamedPipe(x) => x.readable().await, } } fn try_read(&mut self, buf: &mut [u8]) -> tokio::io::Result { match self { Bearer::Tcp(x) => x.try_read(buf), - #[cfg(not(target_os = "windows"))] + + #[cfg(unix)] Bearer::Unix(x) => x.try_read(buf), + + #[cfg(windows)] + Bearer::NamedPipe(x) => x.try_read(buf), } } async fn write_all(&mut self, buf: &[u8]) -> tokio::io::Result<()> { match self { Bearer::Tcp(x) => x.write_all(buf).await, - #[cfg(not(target_os = "windows"))] + + #[cfg(unix)] Bearer::Unix(x) => x.write_all(buf).await, + + #[cfg(windows)] + Bearer::NamedPipe(x) => x.write_all(buf).await, } } async fn flush(&mut self) -> tokio::io::Result<()> { match self { Bearer::Tcp(x) => x.flush().await, - #[cfg(not(target_os = "windows"))] + + #[cfg(unix)] Bearer::Unix(x) => x.flush().await, + + #[cfg(windows)] + Bearer::NamedPipe(x) => x.flush().await, } } } @@ -493,7 +541,7 @@ mod tests { let channel = AgentChannel::for_client(0, &ingress, &egress); - egress.0.send((0 ^ 0x8000, input)).unwrap(); + egress.0.send((0x8000, input)).unwrap(); let mut buf = ChannelBuffer::new(channel); @@ -517,7 +565,7 @@ mod tests { while !input.is_empty() { let chunk = Vec::from(input.drain(0..2).as_slice()); - egress.0.send((0 ^ 0x8000, chunk)).unwrap(); + egress.0.send((0x8000, chunk)).unwrap(); } let mut buf = ChannelBuffer::new(channel); diff --git a/pallas-network/tests/plexer.rs b/pallas-network/tests/plexer.rs index 2f03d968..8ae07204 100644 --- a/pallas-network/tests/plexer.rs +++ b/pallas-network/tests/plexer.rs @@ -9,9 +9,9 @@ async fn setup_passive_muxer() -> Plexer { .await .unwrap(); - println!("listening for connections on port {}", P); + println!("listening for connections on port {P}"); - let (bearer, _) = Bearer::accept_tcp(server).await.unwrap(); + let (bearer, _) = Bearer::accept_tcp(&server).await.unwrap(); Plexer::new(bearer) } diff --git a/pallas-network/tests/protocols.rs b/pallas-network/tests/protocols.rs index 8f6d9223..8c129fd5 100644 --- a/pallas-network/tests/protocols.rs +++ b/pallas-network/tests/protocols.rs @@ -1,9 +1,22 @@ -use pallas_network::facades::PeerClient; +use std::fs; +use std::net::{Ipv4Addr, SocketAddrV4}; +use std::time::Duration; + +use pallas_codec::utils::AnyCbor; +use pallas_network::facades::{NodeClient, PeerClient, PeerServer}; +use pallas_network::miniprotocols::blockfetch::BlockRequest; +use pallas_network::miniprotocols::chainsync::{ClientRequest, HeaderContent, Tip}; +use pallas_network::miniprotocols::handshake::n2n::VersionData; +use pallas_network::miniprotocols::localstate::ClientQueryRequest; use pallas_network::miniprotocols::{ blockfetch, chainsync::{self, NextResponse}, Point, }; +use pallas_network::miniprotocols::{handshake, localstate}; +use pallas_network::multiplexer::{Bearer, Plexer}; +use std::path::Path; +use tokio::net::{TcpListener, UnixListener}; #[tokio::test] #[ignore] @@ -24,7 +37,7 @@ pub async fn chainsync_history_happy_path() { .await .unwrap(); - println!("{:?}", point); + println!("{point:?}"); assert!(matches!(client.state(), chainsync::State::Idle)); @@ -121,7 +134,7 @@ pub async fn blockfetch_happy_path() { println!("streaming..."); - assert!(matches!(range_ok, Ok(_))); + assert!(range_ok.is_ok()); for _ in 0..1 { let next = client.recv_while_streaming().await.unwrap(); @@ -136,11 +149,444 @@ pub async fn blockfetch_happy_path() { let next = client.recv_while_streaming().await.unwrap(); - assert!(matches!(next, None)); + assert!(next.is_none()); client.send_done().await.unwrap(); assert!(matches!(client.state(), blockfetch::State::Done)); } -// TODO: redo txsubmission client test +#[tokio::test] +#[ignore] +pub async fn blockfetch_server_and_client_happy_path() { + let block_bodies = vec![ + hex::decode("deadbeefdeadbeef").unwrap(), + hex::decode("c0ffeec0ffeec0ffee").unwrap(), + ]; + + let point = Point::Specific( + 1337, + hex::decode("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(), + ); + + let server = tokio::spawn({ + let bodies = block_bodies.clone(); + let point = point.clone(); + async move { + // server setup + + let server_listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 30001)) + .await + .unwrap(); + + let mut peer_server = PeerServer::accept(&server_listener, 0).await.unwrap(); + + let server_bf = peer_server.blockfetch(); + + // server receives range from client, sends blocks + + let BlockRequest(range_request) = server_bf.recv_while_idle().await.unwrap().unwrap(); + + assert_eq!(range_request, (point.clone(), point.clone())); + assert_eq!(*server_bf.state(), blockfetch::State::Busy); + + server_bf.send_block_range(bodies).await.unwrap(); + + assert_eq!(*server_bf.state(), blockfetch::State::Idle); + + // server receives range from client, sends NoBlocks + + let BlockRequest(_) = server_bf.recv_while_idle().await.unwrap().unwrap(); + + server_bf.send_block_range(vec![]).await.unwrap(); + + assert_eq!(*server_bf.state(), blockfetch::State::Idle); + + assert!(server_bf.recv_while_idle().await.unwrap().is_none()); + + assert_eq!(*server_bf.state(), blockfetch::State::Done); + } + }); + + let client = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + + // client setup + + let mut client_to_server_conn = PeerClient::connect("localhost:30001", 0).await.unwrap(); + + let client_bf = client_to_server_conn.blockfetch(); + + // client sends request range + + client_bf + .send_request_range((point.clone(), point.clone())) + .await + .unwrap(); + + assert!(client_bf.recv_while_busy().await.unwrap().is_some()); + + // client receives blocks until idle + + let mut received_bodies = Vec::new(); + + while let Some(received_body) = client_bf.recv_while_streaming().await.unwrap() { + received_bodies.push(received_body) + } + + assert_eq!(received_bodies, block_bodies); + + // client sends request range + + client_bf + .send_request_range((point.clone(), point.clone())) + .await + .unwrap(); + + // recv_while_busy returns None for NoBlocks message + assert!(client_bf.recv_while_busy().await.unwrap().is_none()); + + // client sends done + + client_bf.send_done().await.unwrap(); + }); + + _ = tokio::join!(client, server); +} + +#[tokio::test] +#[ignore] +pub async fn chainsync_server_and_client_happy_path_n2n() { + let point1 = Point::Specific(1, vec![0x01]); + let point2 = Point::Specific(2, vec![0x02]); + + let server = tokio::spawn({ + let point1 = point1.clone(); + let point2 = point2.clone(); + async move { + // server setup + + let server_listener = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 30001)) + .await + .unwrap(); + + let (bearer, _) = Bearer::accept_tcp(&server_listener).await.unwrap(); + + let mut server_plexer = Plexer::new(bearer); + + let mut server_hs: handshake::Server = + handshake::Server::new(server_plexer.subscribe_server(0)); + let mut server_cs = chainsync::N2NServer::new(server_plexer.subscribe_server(2)); + + tokio::spawn(async move { server_plexer.run().await }); + + server_hs.receive_proposed_versions().await.unwrap(); + server_hs + .accept_version(10, VersionData::new(0, false)) + .await + .unwrap(); + + // server receives find intersect from client, sends intersect point + + assert_eq!(*server_cs.state(), chainsync::State::Idle); + + let intersect_points = match server_cs.recv_while_idle().await.unwrap().unwrap() { + ClientRequest::Intersect(points) => points, + ClientRequest::RequestNext => panic!("unexpected message"), + }; + + assert_eq!(*server_cs.state(), chainsync::State::Intersect); + assert_eq!(intersect_points, vec![point2.clone(), point1.clone()]); + + server_cs + .send_intersect_found(point2.clone(), Tip(point2.clone(), 1337)) + .await + .unwrap(); + + assert_eq!(*server_cs.state(), chainsync::State::Idle); + + // server receives request next from client, sends rollbackwards + + match server_cs.recv_while_idle().await.unwrap().unwrap() { + ClientRequest::RequestNext => (), + ClientRequest::Intersect(_) => panic!("unexpected message"), + }; + + assert_eq!(*server_cs.state(), chainsync::State::CanAwait); + + server_cs + .send_roll_backward(point2.clone(), Tip(point2.clone(), 1337)) + .await + .unwrap(); + + assert_eq!(*server_cs.state(), chainsync::State::Idle); + + // server receives request next from client, sends rollforwards + + match server_cs.recv_while_idle().await.unwrap().unwrap() { + ClientRequest::RequestNext => (), + ClientRequest::Intersect(_) => panic!("unexpected message"), + }; + + assert_eq!(*server_cs.state(), chainsync::State::CanAwait); + + let header2 = HeaderContent { + variant: 1, + byron_prefix: None, + cbor: hex::decode("c0ffeec0ffeec0ffee").unwrap(), + }; + + server_cs + .send_roll_forward(header2, Tip(point2.clone(), 1337)) + .await + .unwrap(); + + assert_eq!(*server_cs.state(), chainsync::State::Idle); + + // server receives request next from client, sends await reply + // then rollforwards + + match server_cs.recv_while_idle().await.unwrap().unwrap() { + ClientRequest::RequestNext => (), + ClientRequest::Intersect(_) => panic!("unexpected message"), + }; + + assert_eq!(*server_cs.state(), chainsync::State::CanAwait); + + server_cs.send_await_reply().await.unwrap(); + + assert_eq!(*server_cs.state(), chainsync::State::MustReply); + + let header1 = HeaderContent { + variant: 1, + byron_prefix: None, + cbor: hex::decode("deadbeefdeadbeef").unwrap(), + }; + + server_cs + .send_roll_forward(header1, Tip(point1.clone(), 123)) + .await + .unwrap(); + + assert_eq!(*server_cs.state(), chainsync::State::Idle); + + // server receives client done + + assert!(server_cs.recv_while_idle().await.unwrap().is_none()); + assert_eq!(*server_cs.state(), chainsync::State::Done); + } + }); + + let client = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(2)).await; + + // client setup + + let mut client_to_server_conn = PeerClient::connect("localhost:30001", 0).await.unwrap(); + + let client_cs = client_to_server_conn.chainsync(); + + // client sends find intersect + + let intersect_response = client_cs + .find_intersect(vec![point2.clone(), point1.clone()]) + .await + .unwrap(); + + assert_eq!(intersect_response.0, Some(point2.clone())); + assert_eq!(intersect_response.1 .0, point2.clone()); + assert_eq!(intersect_response.1 .1, 1337); + + // client sends msg request next + + client_cs.send_request_next().await.unwrap(); + + // client receives rollback + + match client_cs.recv_while_can_await().await.unwrap() { + NextResponse::RollBackward(point, tip) => { + assert_eq!(point, point2.clone()); + assert_eq!(tip.0, point2.clone()); + assert_eq!(tip.1, 1337); + } + _ => panic!("unexpected response"), + } + + client_cs.send_request_next().await.unwrap(); + + // client receives roll forward + + match client_cs.recv_while_can_await().await.unwrap() { + NextResponse::RollForward(content, tip) => { + assert_eq!(content.cbor, hex::decode("c0ffeec0ffeec0ffee").unwrap()); + assert_eq!(tip.0, point2.clone()); + assert_eq!(tip.1, 1337); + } + _ => panic!("unexpected response"), + } + + // client sends msg request next + + client_cs.send_request_next().await.unwrap(); + + // client receives await + + match client_cs.recv_while_can_await().await.unwrap() { + NextResponse::Await => (), + _ => panic!("unexpected response"), + } + + match client_cs.recv_while_must_reply().await.unwrap() { + NextResponse::RollForward(content, tip) => { + assert_eq!(content.cbor, hex::decode("deadbeefdeadbeef").unwrap()); + assert_eq!(tip.0, point1.clone()); + assert_eq!(tip.1, 123); + } + _ => panic!("unexpected response"), + } + + // client sends done + + client_cs.send_done().await.unwrap(); + }); + + _ = tokio::join!(client, server); +} + +#[tokio::test] +#[ignore] +pub async fn local_state_query_server_and_client_happy_path() { + let server = tokio::spawn({ + async move { + // server setup + let socket_path = Path::new("node.socket"); + + if socket_path.exists() { + fs::remove_file(&socket_path).unwrap(); + } + + let unix_listener = UnixListener::bind(socket_path).unwrap(); + + let mut server = pallas_network::facades::NodeServer::accept(&unix_listener, 0) + .await + .unwrap(); + + // wait for acquire request from client + + let maybe_acquire = server.statequery().recv_while_idle().await.unwrap(); + + assert!(maybe_acquire.is_some()); + assert_eq!(*server.statequery().state(), localstate::State::Acquiring); + + server.statequery().send_acquired().await.unwrap(); + + assert_eq!(*server.statequery().state(), localstate::State::Acquired); + + // server receives query from client + + let query: localstate::queries_v16::Request = + match server.statequery().recv_while_acquired().await.unwrap() { + ClientQueryRequest::Query(q) => q.into_decode().unwrap(), + x => panic!("unexpected message from client: {x:?}"), + }; + + assert_eq!(query, localstate::queries_v16::Request::GetSystemStart); + assert_eq!(*server.statequery().state(), localstate::State::Querying); + + let result = AnyCbor::from_encode(localstate::queries_v16::SystemStart { + year: 2020, + day_of_year: 1, + picoseconds_of_day: 999999999, + }); + + server.statequery().send_result(result).await.unwrap(); + + assert_eq!(*server.statequery().state(), localstate::State::Acquired); + + // server receives re-acquire from the client + + let maybe_point = match server.statequery().recv_while_acquired().await.unwrap() { + ClientQueryRequest::ReAcquire(p) => p, + x => panic!("unexpected message from client: {x:?}"), + }; + + assert_eq!(maybe_point, Some(Point::Specific(1337, vec![1, 2, 3]))); + assert_eq!(*server.statequery().state(), localstate::State::Acquiring); + + server.statequery().send_acquired().await.unwrap(); + + // server receives release from the client + + match server.statequery().recv_while_acquired().await.unwrap() { + ClientQueryRequest::Release => (), + x => panic!("unexpected message from client: {x:?}"), + }; + + let next_request = server.statequery().recv_while_idle().await.unwrap(); + + assert!(next_request.is_none()); + assert_eq!(*server.statequery().state(), localstate::State::Done); + } + }); + + let client = tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(1)).await; + + // client setup + + let socket_path = "node.socket"; + + let mut client = NodeClient::connect(socket_path, 0).await.unwrap(); + + // client sends acquire + + client + .statequery() + .send_acquire(Some(Point::Origin)) + .await + .unwrap(); + + client.statequery().recv_while_acquiring().await.unwrap(); + + assert_eq!(*client.statequery().state(), localstate::State::Acquired); + + // client sends a BlockQuery + + let request = AnyCbor::from_encode(localstate::queries_v16::Request::GetSystemStart); + + client.statequery().send_query(request).await.unwrap(); + + let result: localstate::queries_v16::SystemStart = client + .statequery() + .recv_while_querying() + .await + .unwrap() + .into_decode() + .unwrap(); + + assert_eq!( + result, + localstate::queries_v16::SystemStart { + year: 2020, + day_of_year: 1, + picoseconds_of_day: 999999999, + } + ); + + // client sends a ReAquire + + client + .statequery() + .send_reacquire(Some(Point::Specific(1337, vec![1, 2, 3]))) + .await + .unwrap(); + + client.statequery().recv_while_acquiring().await.unwrap(); + + client.statequery().send_release().await.unwrap(); + + client.statequery().send_done().await.unwrap(); + }); + + _ = tokio::join!(client, server); +} diff --git a/pallas-primitives/Cargo.toml b/pallas-primitives/Cargo.toml index 87c2a959..19c79c7a 100644 --- a/pallas-primitives/Cargo.toml +++ b/pallas-primitives/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "pallas-primitives" description = "Ledger primitives and cbor codec for the different Cardano eras" -version = "0.19.0-alpha.1" +version = "0.19.1" edition = "2021" repository = "https://github.com/txpipe/pallas" homepage = "https://github.com/txpipe/pallas" @@ -16,8 +16,8 @@ authors = [ [dependencies] hex = "0.4.3" log = "0.4.14" -pallas-crypto = { version = "0.19.0-alpha.0", path = "../pallas-crypto" } -pallas-codec = { version = "0.19.0-alpha.0", path = "../pallas-codec" } +pallas-crypto = { version = "=0.19.1", path = "../pallas-crypto" } +pallas-codec = { version = "=0.19.1", path = "../pallas-codec" } base58 = "0.2.0" bech32 = "0.9.0" serde = { version = "1.0.136", optional = true, features = ["derive"] } diff --git a/pallas-primitives/src/alonzo/json.rs b/pallas-primitives/src/alonzo/json.rs index 3f2c2e6a..31bd9690 100644 --- a/pallas-primitives/src/alonzo/json.rs +++ b/pallas-primitives/src/alonzo/json.rs @@ -88,7 +88,7 @@ mod tests { #[test] fn test_datums_serialize_as_expected() { - let test_blocks = vec![( + let test_blocks = [( include_str!("../../../test_data/alonzo9.block"), include_str!("../../../test_data/alonzo9.datums"), )]; @@ -118,7 +118,7 @@ mod tests { #[test] fn test_native_scripts_serialize_as_expected() { - let test_blocks = vec![( + let test_blocks = [( include_str!("../../../test_data/alonzo9.block"), include_str!("../../../test_data/alonzo9.native"), )]; diff --git a/pallas-primitives/src/alonzo/model.rs b/pallas-primitives/src/alonzo/model.rs index cba8858c..efc08a28 100644 --- a/pallas-primitives/src/alonzo/model.rs +++ b/pallas-primitives/src/alonzo/model.rs @@ -3,7 +3,7 @@ //! Handcrafted, idiomatic rust artifacts based on based on the [Alonzo CDDL](https://github.com/input-output-hk/cardano-ledger/blob/master/eras/alonzo/test-suite/cddl-files/alonzo.cddl) file in IOHK repo. use serde::{Deserialize, Serialize}; -use std::{fmt, ops::Deref}; +use std::{fmt, hash::Hash as StdHash, ops::Deref}; use pallas_codec::minicbor::{data::Tag, Decode, Encode}; use pallas_crypto::hash::Hash; @@ -78,7 +78,9 @@ pub struct Header { pub body_signature: Bytes, } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive( + Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, StdHash, +)] pub struct TransactionInput { #[n(0)] pub transaction_id: Hash<32>, @@ -375,7 +377,7 @@ pub type Scripthash = Hash<28>; #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct RationalNumber { - pub numerator: i64, + pub numerator: u64, pub denominator: u64, } @@ -1639,7 +1641,7 @@ mod tests { #[test] fn header_isomorphic_decoding_encoding() { - let test_headers = vec![ + let test_headers = [ // peculiar alonzo header used as origin for a vasil devnet include_str!("../../../test_data/alonzo26.header"), ]; diff --git a/pallas-primitives/src/babbage/model.rs b/pallas-primitives/src/babbage/model.rs index edc1f6ca..959243a8 100644 --- a/pallas-primitives/src/babbage/model.rs +++ b/pallas-primitives/src/babbage/model.rs @@ -1,4 +1,4 @@ -//! Ledger primitives and cbor codec for the Alonzo era +//! Ledger primitives and cbor codec for the Babbage era //! //! Handcrafted, idiomatic rust artifacts based on based on the [Babbage CDDL](https://github.com/input-output-hk/cardano-ledger/blob/master/eras/babbage/test-suite/cddl-files/babbage.cddl) file in IOHK repo. @@ -745,7 +745,7 @@ mod tests { #[test] fn block_isomorphic_decoding_encoding() { - let test_blocks = vec![ + let test_blocks = [ include_str!("../../../test_data/babbage1.block"), include_str!("../../../test_data/babbage2.block"), include_str!("../../../test_data/babbage3.block"), @@ -761,6 +761,8 @@ mod tests { include_str!("../../../test_data/babbage8.block"), // block with inline datum that fails hashes include_str!("../../../test_data/babbage9.block"), + // block with pool margin numerator greater than i64::MAX + include_str!("../../../test_data/babbage10.block"), ]; for (idx, block_str) in test_blocks.iter().enumerate() { @@ -768,10 +770,10 @@ mod tests { let bytes = hex::decode(block_str).unwrap_or_else(|_| panic!("bad block file {idx}")); let block: BlockWrapper = minicbor::decode(&bytes[..]) - .unwrap_or_else(|_| panic!("error decoding cbor for file {idx}")); + .unwrap_or_else(|e| panic!("error decoding cbor for file {idx}: {e:?}")); let bytes2 = minicbor::to_vec(block) - .unwrap_or_else(|_| panic!("error encoding block cbor for file {idx}")); + .unwrap_or_else(|e| panic!("error encoding block cbor for file {idx}: {e:?}")); assert!(bytes.eq(&bytes2), "re-encoded bytes didn't match original"); } diff --git a/pallas-primitives/src/byron/model.rs b/pallas-primitives/src/byron/model.rs index 68db4ab6..f5e8cdf5 100644 --- a/pallas-primitives/src/byron/model.rs +++ b/pallas-primitives/src/byron/model.rs @@ -12,6 +12,8 @@ use pallas_codec::utils::{ // required for derive attrs to work use pallas_codec::minicbor; +use std::hash::Hash as StdHash; + // Basic Cardano Types pub type Blake2b256 = Hash<32>; @@ -58,13 +60,13 @@ pub struct Address { pub payload: TagWrap, #[n(1)] - pub crc: u64, + pub crc: u32, } // Transactions // txout = [address, u64] -#[derive(Debug, Encode, Decode, Clone)] +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)] pub struct TxOut { #[n(0)] pub address: Address, @@ -73,7 +75,7 @@ pub struct TxOut { pub amount: u64, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, StdHash)] pub enum TxIn { // [0, #6.24(bytes .cbor ([txid, u32]))] Variant0(CborWrap<(TxId, u32)>), @@ -121,7 +123,7 @@ impl minicbor::Encode for TxIn { } // tx = [[+ txin], [+ txout], attributes] -#[derive(Debug, Encode, Decode, Clone)] +#[derive(Debug, Encode, Decode, Clone, PartialEq, Eq)] pub struct Tx { #[n(0)] pub inputs: MaybeIndefArray, @@ -829,7 +831,7 @@ mod tests { fn boundary_block_isomorphic_decoding_encoding() { type BlockWrapper = (u16, EbBlock); - let test_blocks = vec![include_str!("../../../test_data/genesis.block")]; + let test_blocks = [include_str!("../../../test_data/genesis.block")]; for (idx, block_str) in test_blocks.iter().enumerate() { println!("decoding test block {}", idx + 1); @@ -849,7 +851,7 @@ mod tests { fn main_block_isomorphic_decoding_encoding() { type BlockWrapper<'b> = (u16, MintedBlock<'b>); - let test_blocks = vec![ + let test_blocks = [ //include_str!("../../../test_data/genesis.block"), include_str!("../../../test_data/byron1.block"), include_str!("../../../test_data/byron2.block"), @@ -876,7 +878,7 @@ mod tests { #[test] fn header_isomorphic_decoding_encoding() { - let subjects = vec![include_str!("../../../test_data/byron1.header")]; + let subjects = [include_str!("../../../test_data/byron1.header")]; for (idx, str) in subjects.iter().enumerate() { println!("decoding test header {}", idx + 1); diff --git a/pallas-primitives/src/conway/defs.cddl b/pallas-primitives/src/conway/defs.cddl new file mode 100644 index 00000000..5fd38a6e --- /dev/null +++ b/pallas-primitives/src/conway/defs.cddl @@ -0,0 +1,594 @@ +; fetched 11 sep 2023 + +block = + [ header + , transaction_bodies : [* transaction_body] + , transaction_witness_sets : [* transaction_witness_set] + , auxiliary_data_set : {* transaction_index => auxiliary_data } + , invalid_transactions : [* transaction_index ] + ]; Valid blocks must also satisfy the following two constraints: + ; 1) the length of transaction_bodies and transaction_witness_sets + ; must be the same + ; 2) every transaction_index must be strictly smaller than the + ; length of transaction_bodies + +transaction = + [ transaction_body + , transaction_witness_set + , bool + , auxiliary_data / null + ] + +transaction_index = uint .size 2 + +header = + [ header_body + , body_signature : $kes_signature + ] + +header_body = + [ block_number : uint + , slot : uint + , prev_hash : $hash32 / null + , issuer_vkey : $vkey + , vrf_vkey : $vrf_vkey + , vrf_result : $vrf_cert ; replaces nonce_vrf and leader_vrf + , block_body_size : uint + , block_body_hash : $hash32 ; merkle triple root + , operational_cert + , protocol_version + ] + +operational_cert = + ( hot_vkey : $kes_vkey + , sequence_number : uint + , kes_period : uint + , sigma : $signature + ) + +next_major_protocol_version = 10 + +major_protocol_version = 1..next_major_protocol_version + +protocol_version = (major_protocol_version, uint) + +transaction_body = + { 0 : set ; inputs + , 1 : [* transaction_output] + , 2 : coin ; fee + , ? 3 : uint ; time to live + , ? 4 : [+ certificate] + , ? 5 : withdrawals + , ? 7 : auxiliary_data_hash + , ? 8 : uint ; validity interval start + , ? 9 : mint + , ? 11 : script_data_hash + , ? 13 : nonempty_set ; collateral inputs + , ? 14 : required_signers + , ? 15 : network_id + , ? 16 : transaction_output ; collateral return + , ? 17 : coin ; total collateral + , ? 18 : nonempty_set ; reference inputs + , ? 19 : voting_procedures ; New; Voting procedures + , ? 20 : [+ proposal_procedure] ; New; Proposal procedures + , ? 21 : coin ; New; current treasury value + , ? 22 : positive_coin ; New; donation + } + +voting_procedures = { + voter => { + gov_action_id => voting_procedure } } + +voting_procedure = + [ vote + , anchor / null + ] + +proposal_procedure = + [ deposit : coin + , reward_account + , gov_action + , anchor + ] + +gov_action = + [ parameter_change_action + // hard_fork_initiation_action + // treasury_withdrawals_action + // no_confidence + // new_committee + // new_constitution + // info_action + ] + +parameter_change_action = (0, gov_action_id / null, protocol_param_update) + +hard_fork_initiation_action = (1, gov_action_id / null, [protocol_version]) + +treasury_withdrawals_action = (2, { $reward_account => coin }) + +no_confidence = (3, gov_action_id / null) + +new_committee = (4, gov_action_id / null, set<$committee_cold_credential>, committee) + +new_constitution = (5, gov_action_id / null, constitution) + +committee = [{ $committee_cold_credential => epoch }, unit_interval] + +constitution = + [ anchor + , scripthash / null + ] + +info_action = 6 + +; Constitutional Committee Hot KeyHash: 0 +; Constitutional Committee Hot ScriptHash: 1 +; DRep KeyHash: 2 +; DRep ScriptHash: 3 +; StakingPool KeyHash: 4 +voter = + [ 0, addr_keyhash + // 1, scripthash + // 2, addr_keyhash + // 3, scripthash + // 4, addr_keyhash + ] + +anchor = + [ anchor_url : url + , anchor_data_hash : $hash32 + ] + +; no - 0 +; yes - 1 +; abstain - 2 +vote = 0 .. 2 + +gov_action_id = + [ transaction_id : $hash32 + , gov_action_index : uint + ] + +required_signers = nonempty_set<$addr_keyhash> + +transaction_input = [ transaction_id : $hash32 + , index : uint + ] + +transaction_output = legacy_transaction_output / post_alonzo_transaction_output + +legacy_transaction_output = + [ address + , amount : value + , ? datum_hash : $hash32 + ] + +post_alonzo_transaction_output = + { 0 : address + , 1 : value + , ? 2 : datum_option ; datum option + , ? 3 : script_ref ; script reference + } + +script_data_hash = $hash32 +; This is a hash of data which may affect evaluation of a script. +; This data consists of: +; - The redeemers from the transaction_witness_set (the value of field 5). +; - The datums from the transaction_witness_set (the value of field 4). +; - The value in the costmdls map corresponding to the script's language +; (in field 18 of protocol_param_update.) +; (In the future it may contain additional protocol parameters.) +; +; Since this data does not exist in contiguous form inside a transaction, it needs +; to be independently constructed by each recipient. +; +; The bytestring which is hashed is the concatenation of three things: +; redeemers || datums || language views +; The redeemers are exactly the data present in the transaction witness set. +; Similarly for the datums, if present. If no datums are provided, the middle +; field is omitted (i.e. it is the empty/null bytestring). +; +; language views CDDL: +; { * language => script_integrity_data } +; +; This must be encoded canonically, using the same scheme as in +; RFC7049 section 3.9: +; - Maps, strings, and bytestrings must use a definite-length encoding +; - Integers must be as small as possible. +; - The expressions for map length, string length, and bytestring length +; must be as short as possible. +; - The keys in the map must be sorted as follows: +; - If two keys have different lengths, the shorter one sorts earlier. +; - If two keys have the same length, the one with the lower value +; in (byte-wise) lexical order sorts earlier. +; +; For PlutusV1 (language id 0), the language view is the following: +; - the value of costmdls map at key 0 (in other words, the script_integrity_data) +; is encoded as an indefinite length list and the result is encoded as a bytestring. +; (our apologies) +; For example, the script_integrity_data corresponding to the all zero costmodel for V1 +; would be encoded as (in hex): +; 58a89f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff +; - the language ID tag is also encoded twice. first as a uint then as +; a bytestring. (our apologies) +; Concretely, this means that the language version for V1 is encoded as +; 4100 in hex. +; For PlutusV2 (language id 1), the language view is the following: +; - the value of costmdls map at key 1 is encoded as an definite length list. +; For example, the script_integrity_data corresponding to the all zero costmodel for V2 +; would be encoded as (in hex): +; 98af0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +; - the language ID tag is encoded as expected. +; Concretely, this means that the language version for V2 is encoded as +; 01 in hex. +; For PlutusV3 (language id 2), the language view is the following: +; - the value of costmdls map at key 2 is encoded as a definite length list. +; +; Note that each Plutus language represented inside a transaction must have +; a cost model in the costmdls protocol parameter in order to execute, +; regardless of what the script integrity data is. +; +; Finally, note that in the case that a transaction includes datums but does not +; include the redeemers field, the script data format becomes (in hex): +; [ 80 | datums | A0 ] +; corresponding to a CBOR empty list and an empty map. +; Note that a transaction might include the redeemers field and it to the +; empty map, in which case the user supplied encoding of the empty map is used. + +; address = bytes +; reward_account = bytes + +; address format: +; [ 8 bit header | payload ]; +; +; shelley payment addresses: +; bit 7: 0 +; bit 6: base/other +; bit 5: pointer/enterprise [for base: stake cred is keyhash/scripthash] +; bit 4: payment cred is keyhash/scripthash +; bits 3-0: network id +; +; reward addresses: +; bits 7-5: 111 +; bit 4: credential is keyhash/scripthash +; bits 3-0: network id +; +; byron addresses: +; bits 7-4: 1000 + +; 0000: base address: keyhash28,keyhash28 +; 0001: base address: scripthash28,keyhash28 +; 0010: base address: keyhash28,scripthash28 +; 0011: base address: scripthash28,scripthash28 +; 0100: pointer address: keyhash28, 3 variable length uint +; 0101: pointer address: scripthash28, 3 variable length uint +; 0110: enterprise address: keyhash28 +; 0111: enterprise address: scripthash28 +; 1000: byron address +; 1110: reward account: keyhash28 +; 1111: reward account: scripthash28 +; 1001 - 1101: future formats + +certificate = + [ stake_registration + // stake_deregistration + // stake_delegation + // pool_registration + // pool_retirement + // reg_cert + // unreg_cert + // vote_deleg_cert + // stake_vote_deleg_cert + // stake_reg_deleg_cert + // vote_reg_deleg_cert + // stake_vote_reg_deleg_cert + // auth_committee_hot_cert + // resign_committee_cold_cert + // reg_drep_cert + // unreg_drep_cert + // update_drep_cert + ] + +stake_registration = (0, stake_credential) ; to be deprecated in era after Conway +stake_deregistration = (1, stake_credential) ; to be deprecated in era after Conway +stake_delegation = (2, stake_credential, pool_keyhash) + +; POOL +pool_registration = (3, pool_params) +pool_retirement = (4, pool_keyhash, epoch) + +; numbers 5 and 6 used to be the Genesis and MIR certificates respectively, +; which were deprecated in Conway + +; DELEG +reg_cert = (7, stake_credential, coin) +unreg_cert = (8, stake_credential, coin) +vote_deleg_cert = (9, stake_credential, drep) +stake_vote_deleg_cert = (10, stake_credential, pool_keyhash, drep) +stake_reg_deleg_cert = (11, stake_credential, pool_keyhash, coin) +vote_reg_deleg_cert = (12, stake_credential, drep, coin) +stake_vote_reg_deleg_cert = (13, stake_credential, pool_keyhash, drep, coin) + +; GOVCERT +auth_committee_hot_cert = (14, committee_cold_credential, committee_hot_credential) +resign_committee_cold_cert = (15, committee_cold_credential) +reg_drep_cert = (16, drep_credential, coin, anchor / null) +unreg_drep_cert = (17, drep_credential, coin) +update_drep_cert = (18, drep_credential, anchor / null) + + +delta_coin = int + +credential = + [ 0, addr_keyhash + // 1, scripthash + ] + +drep = + [ 0, addr_keyhash + // 1, scripthash + // 2 ; always abstain + // 3 ; always no confidence + ] + +stake_credential = credential +drep_credential = credential +committee_cold_credential = credential +committee_hot_credential = credential + +pool_params = ( operator: pool_keyhash + , vrf_keyhash: vrf_keyhash + , pledge: coin + , cost: coin + , margin: unit_interval + , reward_account: reward_account + , pool_owners: set + , relays: [* relay] + , pool_metadata: pool_metadata / null + ) + +port = uint .le 65535 +ipv4 = bytes .size 4 +ipv6 = bytes .size 16 +dns_name = tstr .size (0..64) + +single_host_addr = ( 0 + , port / null + , ipv4 / null + , ipv6 / null + ) +single_host_name = ( 1 + , port / null + , dns_name ; An A or AAAA DNS record + ) +multi_host_name = ( 2 + , dns_name ; A SRV DNS record + ) +relay = + [ single_host_addr + // single_host_name + // multi_host_name + ] + +pool_metadata = [url, pool_metadata_hash] +url = tstr .size (0..64) + +withdrawals = { + reward_account => coin } + +protocol_param_update = + { ? 0: uint ; minfee A + , ? 1: uint ; minfee B + , ? 2: uint ; max block body size + , ? 3: uint ; max transaction size + , ? 4: uint ; max block header size + , ? 5: coin ; key deposit + , ? 6: coin ; pool deposit + , ? 7: epoch ; maximum epoch + , ? 8: uint ; n_opt: desired number of stake pools + , ? 9: rational ; pool pledge influence + , ? 10: unit_interval ; expansion rate + , ? 11: unit_interval ; treasury growth rate + , ? 16: coin ; min pool cost + , ? 17: coin ; ada per utxo byte + , ? 18: costmdls ; cost models for script languages + , ? 19: ex_unit_prices ; execution costs + , ? 20: ex_units ; max tx ex units + , ? 21: ex_units ; max block ex units + , ? 22: uint ; max value size + , ? 23: uint ; collateral percentage + , ? 24: uint ; max collateral inputs + , ? 25: pool_voting_thresholds ; pool voting thresholds + , ? 26: drep_voting_thresholds ; DRep voting thresholds + , ? 27: uint ; min committee size + , ? 28: uint ; committee term limit + , ? 29: epoch ; governance action validity period + , ? 30: coin ; governance action deposit + , ? 31: coin ; DRep deposit + , ? 32: epoch ; DRep inactivity period + } + +pool_voting_thresholds = + [ unit_interval ; motion no confidence + , unit_interval ; committee normal + , unit_interval ; committee no confidence + , unit_interval ; hard fork initiation + ] + +drep_voting_thresholds = + [ unit_interval ; motion no confidence + , unit_interval ; committee normal + , unit_interval ; committee no confidence + , unit_interval ; update constitution + , unit_interval ; hard fork initiation + , unit_interval ; PP network group + , unit_interval ; PP economic group + , unit_interval ; PP technical group + , unit_interval ; PP governance group + , unit_interval ; treasury withdrawal + ] + +transaction_witness_set = + { ? 0: [* vkeywitness ] + , ? 1: [* native_script ] + , ? 2: [* bootstrap_witness ] + , ? 3: [* plutus_v1_script ] + , ? 4: [* plutus_data ] + , ? 5: [* redeemer ] + , ? 6: [* plutus_v2_script ] + , ? 7: [* plutus_v3_script ] + } + +plutus_v1_script = bytes +plutus_v2_script = bytes +plutus_v3_script = bytes + +plutus_data = + constr + / { * plutus_data => plutus_data } + / [ * plutus_data ] + / big_int + / bounded_bytes + +big_int = int / big_uint / big_nint +big_uint = #6.2(bounded_bytes) +big_nint = #6.3(bounded_bytes) + +constr = + #6.121([* a]) + / #6.122([* a]) + / #6.123([* a]) + / #6.124([* a]) + / #6.125([* a]) + / #6.126([* a]) + / #6.127([* a]) + ; similarly for tag range: 6.1280 .. 6.1400 inclusive + / #6.102([uint, [* a]]) + +redeemer = [ tag: redeemer_tag, index: uint, data: plutus_data, ex_units: ex_units ] +redeemer_tag = + 0 ; inputTag "Spend" + / 1 ; mintTag "Mint" + / 2 ; certTag "Cert" + / 3 ; wdrlTag "Reward" + ; TODO / 4 ; drepTag "DRep" +ex_units = [mem: uint, steps: uint] + +ex_unit_prices = + [ mem_price: sub_coin, step_price: sub_coin ] + +language = 0 ; Plutus v1 + / 1 ; Plutus v2 + / 2 ; Plutus v3 + +potential_languages = 0 .. 255 + +; The format for costmdls is flexible enough to allow adding Plutus built-ins and language +; versions in the future. +; +; To construct valid cost models, however, you must restrict to: +; +; { ? 0 : [ 166* int ] ; Plutus v1, only 166 integers are used, but more are accepted (and ignored) +; , ? 1 : [ 175* int ] ; Plutus v2, only 175 integers are used, but more are accepted (and ignored) +; , ? 2 : [ 179* int ] ; Plutus v3, only 179 integers are used, but more are accepted (and ignored) +; } +costmdls = { * potential_languages => [int] } + +transaction_metadatum = + { * transaction_metadatum => transaction_metadatum } + / [ * transaction_metadatum ] + / int + / bytes .size (0..64) + / text .size (0..64) + +transaction_metadatum_label = uint +metadata = { * transaction_metadatum_label => transaction_metadatum } + +auxiliary_data = + metadata ; Shelley + / [ transaction_metadata: metadata ; Shelley-ma + , auxiliary_scripts: [ * native_script ] + ] + / #6.259({ ? 0 => metadata ; Alonzo and beyond + , ? 1 => [ * native_script ] + , ? 2 => [ * plutus_v1_script ] + , ? 3 => [ * plutus_v2_script ] + , ? 4 => [ * plutus_v3_script ] + }) + +vkeywitness = [ $vkey, $signature ] + +bootstrap_witness = + [ public_key : $vkey + , signature : $signature + , chain_code : bytes .size 32 + , attributes : bytes + ] + +native_script = + [ script_pubkey + // script_all + // script_any + // script_n_of_k + // invalid_before + ; Timelock validity intervals are half-open intervals [a, b). + ; This field specifies the left (included) endpoint a. + // invalid_hereafter + ; Timelock validity intervals are half-open intervals [a, b). + ; This field specifies the right (excluded) endpoint b. + ] + +script_pubkey = (0, addr_keyhash) +script_all = (1, [ * native_script ]) +script_any = (2, [ * native_script ]) +script_n_of_k = (3, n: uint, [ * native_script ]) +invalid_before = (4, uint) +invalid_hereafter = (5, uint) + +coin = uint + +sub_coin = positive_interval + +multiasset = { + policy_id => { + asset_name => a } } +policy_id = scripthash +asset_name = bytes .size (0..32) + +negInt64 = -9223372036854775808 .. -1 +posInt64 = 1 .. 9223372036854775807 +nonZeroInt64 = negInt64 / posInt64 ; this is the same as the current int64 definition but without zero + +positive_coin = 1 .. 18446744073709551615 + +value = positive_coin / [positive_coin,multiasset] + +mint = multiasset + +int64 = -9223372036854775808 .. 9223372036854775807 + +network_id = 0 / 1 + +epoch = uint + +addr_keyhash = $hash28 +pool_keyhash = $hash28 + +vrf_keyhash = $hash32 +auxiliary_data_hash = $hash32 +pool_metadata_hash = $hash32 + +; To compute a script hash, note that you must prepend +; a tag to the bytes of the script before hashing. +; The tag is determined by the language. +; The tags in the Conway era are: +; "\x00" for multisig scripts +; "\x01" for Plutus V1 scripts +; "\x02" for Plutus V2 scripts +; "\x03" for Plutus V3 scripts +scripthash = $hash28 + +datum_hash = $hash32 +data = #6.24(bytes .cbor plutus_data) + +datum_option = [ 0, $hash32 // 1, data ] + +script_ref = #6.24(bytes .cbor script) + +script = [ 0, native_script // 1, plutus_v1_script // 2, plutus_v2_script // 3, plutus_v3_script ] \ No newline at end of file diff --git a/pallas-primitives/src/conway/mod.rs b/pallas-primitives/src/conway/mod.rs new file mode 100644 index 00000000..4a7ebf60 --- /dev/null +++ b/pallas-primitives/src/conway/mod.rs @@ -0,0 +1,3 @@ +mod model; + +pub use model::*; diff --git a/pallas-primitives/src/conway/model.rs b/pallas-primitives/src/conway/model.rs new file mode 100644 index 00000000..385955cf --- /dev/null +++ b/pallas-primitives/src/conway/model.rs @@ -0,0 +1,1580 @@ +//! Ledger primitives and cbor codec for the Conway era +//! +//! Handcrafted, idiomatic rust artifacts based on based on the [Conway CDDL](https://github.com/input-output-hk/cardano-ledger/blob/master/eras/conway/test-suite/cddl-files/conway.cddl) file in IOHK repo. + +use serde::{Deserialize, Serialize}; + +use pallas_codec::minicbor::{Decode, Encode}; +use pallas_crypto::hash::Hash; + +use pallas_codec::utils::{Bytes, CborWrap, KeepRaw, KeyValuePairs, MaybeIndefArray, Nullable}; + +// required for derive attrs to work +use pallas_codec::minicbor; + +pub use crate::alonzo::VrfCert; + +pub use crate::babbage::HeaderBody; + +pub use crate::babbage::OperationalCert; + +pub use crate::alonzo::ProtocolVersion; + +pub use crate::alonzo::KesSignature; + +pub use crate::babbage::Header; + +pub use crate::alonzo::TransactionInput; + +pub use crate::alonzo::NonceVariant; + +pub use crate::alonzo::Nonce; + +pub use crate::alonzo::ScriptHash; + +pub use crate::alonzo::PolicyId; + +pub use crate::alonzo::AssetName; + +pub use crate::alonzo::Multiasset; + +pub use crate::alonzo::Mint; + +pub use crate::alonzo::Coin; + +pub use crate::alonzo::Value; + +pub use crate::alonzo::TransactionOutput as LegacyTransactionOutput; + +pub use crate::alonzo::PoolKeyhash; + +pub use crate::alonzo::Epoch; + +pub use crate::alonzo::Genesishash; + +pub use crate::alonzo::GenesisDelegateHash; + +pub use crate::alonzo::VrfKeyhash; + +pub use crate::alonzo::InstantaneousRewardSource; + +pub use crate::alonzo::InstantaneousRewardTarget; + +pub use crate::alonzo::MoveInstantaneousReward; + +pub use crate::alonzo::RewardAccount; + +pub type Withdrawals = KeyValuePairs; + +pub type RequiredSigners = Vec; + +pub use crate::alonzo::Port; + +pub use crate::alonzo::IPv4; + +pub use crate::alonzo::IPv6; + +pub use crate::alonzo::DnsName; + +pub use crate::alonzo::Relay; + +pub use crate::alonzo::PoolMetadataHash; + +pub use crate::alonzo::PoolMetadata; + +pub use crate::alonzo::AddrKeyhash; + +pub use crate::alonzo::Scripthash; + +pub use crate::alonzo::RationalNumber; + +pub use crate::alonzo::UnitInterval; + +pub use crate::alonzo::PositiveInterval; + +pub use crate::alonzo::StakeCredential; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum Certificate { + StakeRegistration(StakeCredential), + StakeDeregistration(StakeCredential), + StakeDelegation(StakeCredential, PoolKeyhash), + PoolRegistration { + operator: PoolKeyhash, + vrf_keyhash: VrfKeyhash, + pledge: Coin, + cost: Coin, + margin: UnitInterval, + reward_account: RewardAccount, + pool_owners: Vec, + relays: Vec, + pool_metadata: Option, + }, + PoolRetirement(PoolKeyhash, Epoch), + + Reg(StakeCredential, Coin), + UnReg(StakeCredential, Coin), + VoteDeleg(StakeCredential, DRep), + StakeVoteDeleg(StakeCredential, PoolKeyhash, DRep), + StakeRegDeleg(StakeCredential, PoolKeyhash, Coin), + VoteRegDeleg(StakeCredential, DRep, Coin), + StakeVoteRegDeleg(StakeCredential, PoolKeyhash, DRep, Coin), + + AuthCommitteeHot(CommitteeColdCredential, CommitteeHotCredential), + ResignCommitteeCold(CommitteeColdCredential), + RegDRepCert(DRepCredential, Coin, Option), + UnRegDRepCert(DRepCredential, Coin), + UpdateDRepCert(StakeCredential, Option), +} + +impl<'b, C> minicbor::decode::Decode<'b, C> for Certificate { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + let variant = d.u16()?; + + match variant { + 0 => { + let a = d.decode_with(ctx)?; + Ok(Certificate::StakeRegistration(a)) + } + 1 => { + let a = d.decode_with(ctx)?; + Ok(Certificate::StakeDeregistration(a)) + } + 2 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::StakeDelegation(a, b)) + } + 3 => { + let operator = d.decode_with(ctx)?; + let vrf_keyhash = d.decode_with(ctx)?; + let pledge = d.decode_with(ctx)?; + let cost = d.decode_with(ctx)?; + let margin = d.decode_with(ctx)?; + let reward_account = d.decode_with(ctx)?; + let pool_owners = d.decode_with(ctx)?; + let relays = d.decode_with(ctx)?; + let pool_metadata = d.decode_with(ctx)?; + + Ok(Certificate::PoolRegistration { + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + }) + } + 4 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::PoolRetirement(a, b)) + } + + 7 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::Reg(a, b)) + } + 8 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::UnReg(a, b)) + } + 9 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::VoteDeleg(a, b)) + } + 10 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + let c = d.decode_with(ctx)?; + Ok(Certificate::StakeVoteDeleg(a, b, c)) + } + 11 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + let c = d.decode_with(ctx)?; + Ok(Certificate::StakeRegDeleg(a, b, c)) + } + 12 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + let c = d.decode_with(ctx)?; + Ok(Certificate::VoteRegDeleg(a, b, c)) + } + 13 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + let c = d.decode_with(ctx)?; + let d = d.decode_with(ctx)?; + Ok(Certificate::StakeVoteRegDeleg(a, b, c, d)) + } + 14 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::AuthCommitteeHot(a, b)) + } + 15 => { + let a = d.decode_with(ctx)?; + Ok(Certificate::ResignCommitteeCold(a)) + } + 16 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + let c = d.decode_with(ctx)?; + Ok(Certificate::RegDRepCert(a, b, c)) + } + 17 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::UnRegDRepCert(a, b)) + } + 18 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(Certificate::UpdateDRepCert(a, b)) + } + _ => Err(minicbor::decode::Error::message( + "unknown variant id for certificate", + )), + } + } +} + +impl minicbor::encode::Encode for Certificate { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + Certificate::StakeRegistration(a) => { + e.array(2)?; + e.u16(0)?; + e.encode_with(a, ctx)?; + } + Certificate::StakeDeregistration(a) => { + e.array(2)?; + e.u16(1)?; + e.encode_with(a, ctx)?; + } + Certificate::StakeDelegation(a, b) => { + e.array(3)?; + e.u16(2)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + Certificate::PoolRegistration { + operator, + vrf_keyhash, + pledge, + cost, + margin, + reward_account, + pool_owners, + relays, + pool_metadata, + } => { + e.array(10)?; + e.u16(3)?; + + e.encode_with(operator, ctx)?; + e.encode_with(vrf_keyhash, ctx)?; + e.encode_with(pledge, ctx)?; + e.encode_with(cost, ctx)?; + e.encode_with(margin, ctx)?; + e.encode_with(reward_account, ctx)?; + e.encode_with(pool_owners, ctx)?; + e.encode_with(relays, ctx)?; + e.encode_with(pool_metadata, ctx)?; + } + Certificate::PoolRetirement(a, b) => { + e.array(3)?; + e.u16(4)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + // 5 and 6 removed in conway + Certificate::Reg(a, b) => { + e.array(3)?; + e.u16(7)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + Certificate::UnReg(a, b) => { + e.array(3)?; + e.u16(8)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + Certificate::VoteDeleg(a, b) => { + e.array(3)?; + e.u16(9)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + Certificate::StakeVoteDeleg(a, b, c) => { + e.array(4)?; + e.u16(10)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + e.encode_with(c, ctx)?; + } + Certificate::StakeRegDeleg(a, b, c) => { + e.array(4)?; + e.u16(11)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + e.encode_with(c, ctx)?; + } + Certificate::VoteRegDeleg(a, b, c) => { + e.array(4)?; + e.u16(12)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + e.encode_with(c, ctx)?; + } + Certificate::StakeVoteRegDeleg(a, b, c, d) => { + e.array(5)?; + e.u16(13)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + e.encode_with(c, ctx)?; + e.encode_with(d, ctx)?; + } + Certificate::AuthCommitteeHot(a, b) => { + e.array(3)?; + e.u16(14)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + Certificate::ResignCommitteeCold(a) => { + e.array(2)?; + e.u16(15)?; + e.encode_with(a, ctx)?; + } + Certificate::RegDRepCert(a, b, c) => { + e.array(4)?; + e.u16(16)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + e.encode_with(c, ctx)?; + } + Certificate::UnRegDRepCert(a, b) => { + e.array(3)?; + e.u16(17)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + Certificate::UpdateDRepCert(a, b) => { + e.array(3)?; + e.u16(18)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + } + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] +pub enum DRep { + Key(AddrKeyhash), + Script(Scripthash), + Abstain, + NoConfidence, +} + +impl<'b, C> minicbor::decode::Decode<'b, C> for DRep { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + let variant = d.u16()?; + + match variant { + 0 => Ok(DRep::Key(d.decode_with(ctx)?)), + 1 => Ok(DRep::Script(d.decode_with(ctx)?)), + 2 => Ok(DRep::Abstain), + 3 => Ok(DRep::NoConfidence), + _ => Err(minicbor::decode::Error::message( + "invalid variant id for DRep", + )), + } + } +} + +impl minicbor::encode::Encode for DRep { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + DRep::Key(h) => { + e.array(2)?; + e.encode_with(0, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + DRep::Script(h) => { + e.array(2)?; + e.encode_with(1, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + DRep::Abstain => { + e.array(1)?; + e.encode_with(2, ctx)?; + + Ok(()) + } + DRep::NoConfidence => { + e.array(1)?; + e.encode_with(3, ctx)?; + + Ok(()) + } + } + } +} + +pub type DRepCredential = StakeCredential; + +pub type CommitteeColdCredential = StakeCredential; + +pub type CommitteeHotCredential = StakeCredential; + +pub use crate::alonzo::NetworkId; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(index_only)] +pub enum Language { + #[n(0)] + PlutusV1, + + #[n(1)] + PlutusV2, + + #[n(2)] + PlutusV3, +} + +pub use crate::alonzo::CostModel; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(map)] +pub struct CostMdls { + #[n(0)] + pub plutus_v1: Option, + + #[n(1)] + pub plutus_v2: Option, + + #[n(2)] + pub plutus_v3: Option, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(map)] +pub struct ProtocolParamUpdate { + #[n(0)] + pub minfee_a: Option, + #[n(1)] + pub minfee_b: Option, + #[n(2)] + pub max_block_body_size: Option, + #[n(3)] + pub max_transaction_size: Option, + #[n(4)] + pub max_block_header_size: Option, + #[n(5)] + pub key_deposit: Option, + #[n(6)] + pub pool_deposit: Option, + #[n(7)] + pub maximum_epoch: Option, + #[n(8)] + pub desired_number_of_stake_pools: Option, + #[n(9)] + pub pool_pledge_influence: Option, + #[n(10)] + pub expansion_rate: Option, + #[n(11)] + pub treasury_growth_rate: Option, + + #[n(16)] + pub min_pool_cost: Option, + #[n(17)] + pub ada_per_utxo_byte: Option, + #[n(18)] + pub cost_models_for_script_languages: Option, + #[n(19)] + pub execution_costs: Option, + #[n(20)] + pub max_tx_ex_units: Option, + #[n(21)] + pub max_block_ex_units: Option, + #[n(22)] + pub max_value_size: Option, + #[n(23)] + pub collateral_percentage: Option, + #[n(24)] + pub max_collateral_inputs: Option, + + #[n(25)] + pub pool_voting_thresholds: Option, + #[n(26)] + pub drep_voting_thresholds: Option, + #[n(27)] + pub min_committee_size: Option, + #[n(28)] + pub committee_term_limit: Option, + #[n(29)] + pub governance_action_validity_period: Option, + #[n(30)] + pub governance_action_deposit: Option, + #[n(31)] + pub drep_deposit: Option, + #[n(32)] + pub drep_inactivity_period: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct PoolVotingThresholds { + pub motion_no_confidence: UnitInterval, + pub committee_normal: UnitInterval, + pub committee_no_confidence: UnitInterval, + pub hard_fork_initiation: UnitInterval, +} + +impl<'b, C> minicbor::Decode<'b, C> for PoolVotingThresholds { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self { + motion_no_confidence: d.decode_with(ctx)?, + committee_normal: d.decode_with(ctx)?, + committee_no_confidence: d.decode_with(ctx)?, + hard_fork_initiation: d.decode_with(ctx)?, + }) + } +} + +impl minicbor::Encode for PoolVotingThresholds { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(4)?; + + e.encode_with(&self.motion_no_confidence, ctx)?; + e.encode_with(&self.committee_normal, ctx)?; + e.encode_with(&self.committee_no_confidence, ctx)?; + e.encode_with(&self.hard_fork_initiation, ctx)?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct DRepVotingThresholds { + pub motion_no_confidence: UnitInterval, + pub committee_normal: UnitInterval, + pub committee_no_confidence: UnitInterval, + pub update_constitution: UnitInterval, + pub hard_fork_initiation: UnitInterval, + pub pp_network_group: UnitInterval, + pub pp_economic_group: UnitInterval, + pub pp_technical_group: UnitInterval, + pub pp_governance_group: UnitInterval, + pub treasury_withdrawal: UnitInterval, +} + +impl<'b, C> minicbor::Decode<'b, C> for DRepVotingThresholds { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self { + motion_no_confidence: d.decode_with(ctx)?, + committee_normal: d.decode_with(ctx)?, + committee_no_confidence: d.decode_with(ctx)?, + update_constitution: d.decode_with(ctx)?, + hard_fork_initiation: d.decode_with(ctx)?, + pp_network_group: d.decode_with(ctx)?, + pp_economic_group: d.decode_with(ctx)?, + pp_technical_group: d.decode_with(ctx)?, + pp_governance_group: d.decode_with(ctx)?, + treasury_withdrawal: d.decode_with(ctx)?, + }) + } +} + +impl minicbor::Encode for DRepVotingThresholds { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(10)?; + + e.encode_with(&self.motion_no_confidence, ctx)?; + e.encode_with(&self.committee_normal, ctx)?; + e.encode_with(&self.committee_no_confidence, ctx)?; + e.encode_with(&self.update_constitution, ctx)?; + e.encode_with(&self.hard_fork_initiation, ctx)?; + e.encode_with(&self.pp_network_group, ctx)?; + e.encode_with(&self.pp_economic_group, ctx)?; + e.encode_with(&self.pp_technical_group, ctx)?; + e.encode_with(&self.pp_governance_group, ctx)?; + e.encode_with(&self.treasury_withdrawal, ctx)?; + + Ok(()) + } +} + +#[derive(Encode, Decode, Debug, PartialEq, Clone)] +#[cbor(map)] +pub struct PseudoTransactionBody { + #[n(0)] + pub inputs: Vec, + + #[n(1)] + pub outputs: Vec, + + #[n(2)] + pub fee: Coin, + + #[n(3)] + pub ttl: Option, + + #[n(4)] + pub certificates: Option>, // TODO: NON EMPTY + + #[n(5)] + pub withdrawals: Option>, // TODO: NON EMPTY + + // #[n(6)] + // pub update: Option, + #[n(7)] + pub auxiliary_data_hash: Option, + + #[n(8)] + pub validity_interval_start: Option, + + #[n(9)] + pub mint: Option>, // TODO: MULTI ASSET NON EMPTY + + #[n(11)] + pub script_data_hash: Option>, + + #[n(13)] + pub collateral: Option>, // TODO: NON EMPTY SET + + #[n(14)] + pub required_signers: Option>, // TODO: NON EMPTY SET + + #[n(15)] + pub network_id: Option, + + #[n(16)] + pub collateral_return: Option, + + #[n(17)] + pub total_collateral: Option, + + #[n(18)] + pub reference_inputs: Option>, // TODO: NON EMPTY SET + + // -- NEW IN CONWAY + #[n(19)] + pub voting_procedures: Option, + + #[n(20)] + pub proposal_procedures: Option>, // TODO: NON EMPTY MAP + + #[n(21)] + pub treasury_value: Option, + + #[n(22)] + pub donation: Option, // TODO: NON ZERO (POSITIVE COIN) +} + +pub type TransactionBody = PseudoTransactionBody; + +pub type MintedTransactionBody<'a> = PseudoTransactionBody>; + +impl<'a> From> for TransactionBody { + fn from(value: MintedTransactionBody<'a>) -> Self { + Self { + inputs: value.inputs, + outputs: value.outputs.into_iter().map(|x| x.into()).collect(), + fee: value.fee, + ttl: value.ttl, + certificates: value.certificates, + withdrawals: value.withdrawals, + auxiliary_data_hash: value.auxiliary_data_hash, + validity_interval_start: value.validity_interval_start, + mint: value.mint, + script_data_hash: value.script_data_hash, + collateral: value.collateral, + required_signers: value.required_signers, + network_id: value.network_id, + collateral_return: value.collateral_return.map(|x| x.into()), + total_collateral: value.total_collateral, + reference_inputs: value.reference_inputs, + voting_procedures: value.voting_procedures, + proposal_procedures: value.proposal_procedures, + treasury_value: value.treasury_value, + donation: value.donation, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum Vote { + No, + Yes, + Abstain, +} + +impl<'b, C> minicbor::Decode<'b, C> for Vote { + fn decode( + d: &mut minicbor::Decoder<'b>, + _ctx: &mut C, + ) -> Result { + match d.u8()? { + 0 => Ok(Self::No), + 1 => Ok(Self::Yes), + 2 => Ok(Self::Abstain), + _ => Err(minicbor::decode::Error::message( + "invalid number for Vote kind", + )), + } + } +} + +impl minicbor::Encode for Vote { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match &self { + Self::No => e.u8(0)?, + Self::Yes => e.u8(1)?, + Self::Abstain => e.u8(2)?, + }; + + Ok(()) + } +} + +pub type VotingProcedures = KeyValuePairs>; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct VotingProcedure { + pub vote: Vote, + pub anchor: Option, +} + +impl<'b, C> minicbor::Decode<'b, C> for VotingProcedure { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self { + vote: d.decode_with(ctx)?, + anchor: d.decode_with(ctx)?, + }) + } +} + +impl minicbor::Encode for VotingProcedure { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + + e.encode_with(&self.vote, ctx)?; + e.encode_with(&self.anchor, ctx)?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct ProposalProcedure { + pub deposit: Coin, + pub reward_account: RewardAccount, + pub gov_action: GovAction, + pub anchor: Anchor, +} + +impl<'b, C> minicbor::Decode<'b, C> for ProposalProcedure { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self { + deposit: d.decode_with(ctx)?, + reward_account: d.decode_with(ctx)?, + gov_action: d.decode_with(ctx)?, + anchor: d.decode_with(ctx)?, + }) + } +} + +impl minicbor::Encode for ProposalProcedure { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(4)?; + + e.encode_with(self.deposit, ctx)?; + e.encode_with(&self.reward_account, ctx)?; + e.encode_with(&self.gov_action, ctx)?; + e.encode_with(&self.anchor, ctx)?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub enum GovAction { + ParameterChange(Option, Box), + HardForkInitiation(Option, Vec), + TreasuryWithdrawals(KeyValuePairs), + NoConfidence(Option), + UpdateCommittee( + Option, + Vec, + KeyValuePairs, + UnitInterval, + ), + NewConstitution(Option, Constitution), + Information, +} + +impl<'b, C> minicbor::decode::Decode<'b, C> for GovAction { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + let variant = d.u16()?; + + match variant { + 0 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(GovAction::ParameterChange(a, b)) + } + 1 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(GovAction::HardForkInitiation(a, b)) + } + 2 => { + let a = d.decode_with(ctx)?; + Ok(GovAction::TreasuryWithdrawals(a)) + } + 3 => { + let a = d.decode_with(ctx)?; + Ok(GovAction::NoConfidence(a)) + } + 4 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + let c = d.decode_with(ctx)?; + let d = d.decode_with(ctx)?; + Ok(GovAction::UpdateCommittee(a, b, c, d)) + } + 5 => { + let a = d.decode_with(ctx)?; + let b = d.decode_with(ctx)?; + Ok(GovAction::NewConstitution(a, b)) + } + 6 => Ok(GovAction::Information), + _ => Err(minicbor::decode::Error::message( + "unknown variant id for certificate", + )), + } + } +} + +impl minicbor::encode::Encode for GovAction { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + GovAction::ParameterChange(a, b) => { + e.array(3)?; + e.u16(0)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + GovAction::HardForkInitiation(a, b) => { + e.array(3)?; + e.u16(1)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + GovAction::TreasuryWithdrawals(a) => { + e.array(2)?; + e.u16(2)?; + e.encode_with(a, ctx)?; + } + GovAction::NoConfidence(a) => { + e.array(2)?; + e.u16(3)?; + e.encode_with(a, ctx)?; + } + GovAction::UpdateCommittee(a, b, c, d) => { + e.array(5)?; + e.u16(4)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + e.encode_with(c, ctx)?; + e.encode_with(d, ctx)?; + } + GovAction::NewConstitution(a, b) => { + e.array(3)?; + e.u16(5)?; + e.encode_with(a, ctx)?; + e.encode_with(b, ctx)?; + } + // TODO: CDDL SAYS JUST "6", no group (array) + GovAction::Information => { + e.array(1)?; + e.u16(6)?; + } + } + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] +pub struct Constitution(Anchor, Option); + +impl<'b, C> minicbor::Decode<'b, C> for Constitution { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self(d.decode_with(ctx)?, d.decode_with(ctx)?)) + } +} + +impl minicbor::Encode for Constitution { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + + e.encode_with(&self.0, ctx)?; + e.encode_with(&self.1, ctx)?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] +pub enum Voter { + ConstitutionalCommitteeKey(AddrKeyhash), + ConstitutionalCommitteeScript(ScriptHash), + DRepKey(AddrKeyhash), + DRepScript(ScriptHash), + StakePoolKey(AddrKeyhash), +} + +impl<'b, C> minicbor::decode::Decode<'b, C> for Voter { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + let variant = d.u16()?; + + match variant { + 0 => Ok(Voter::ConstitutionalCommitteeKey(d.decode_with(ctx)?)), + 1 => Ok(Voter::ConstitutionalCommitteeScript(d.decode_with(ctx)?)), + 2 => Ok(Voter::DRepKey(d.decode_with(ctx)?)), + 3 => Ok(Voter::DRepScript(d.decode_with(ctx)?)), + 4 => Ok(Voter::StakePoolKey(d.decode_with(ctx)?)), + _ => Err(minicbor::decode::Error::message( + "invalid variant id for DRep", + )), + } + } +} + +impl minicbor::encode::Encode for Voter { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + + match self { + Voter::ConstitutionalCommitteeKey(h) => { + e.encode_with(0, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + Voter::ConstitutionalCommitteeScript(h) => { + e.encode_with(1, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + Voter::DRepKey(h) => { + e.encode_with(2, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + Voter::DRepScript(h) => { + e.encode_with(3, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + Voter::StakePoolKey(h) => { + e.encode_with(4, ctx)?; + e.encode_with(h, ctx)?; + + Ok(()) + } + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord, Clone)] +pub struct Anchor(String, Hash<32>); + +impl<'b, C> minicbor::Decode<'b, C> for Anchor { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self(d.decode_with(ctx)?, d.decode_with(ctx)?)) + } +} + +impl minicbor::Encode for Anchor { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + + e.encode_with(&self.0, ctx)?; + e.encode_with(self.1, ctx)?; + + Ok(()) + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct GovActionId(Hash<32>, u32); + +impl<'b, C> minicbor::Decode<'b, C> for GovActionId { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + Ok(Self(d.decode_with(ctx)?, d.decode_with(ctx)?)) + } +} + +impl minicbor::Encode for GovActionId { + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.array(2)?; + + e.encode_with(self.0, ctx)?; + e.encode_with(self.1, ctx)?; + + Ok(()) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum PseudoTransactionOutput { + Legacy(LegacyTransactionOutput), + PostAlonzo(T), +} + +impl<'b, C, T> minicbor::Decode<'b, C> for PseudoTransactionOutput +where + T: minicbor::Decode<'b, C>, +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + match d.datatype()? { + minicbor::data::Type::Array | minicbor::data::Type::ArrayIndef => { + Ok(PseudoTransactionOutput::Legacy(d.decode_with(ctx)?)) + } + minicbor::data::Type::Map | minicbor::data::Type::MapIndef => { + Ok(PseudoTransactionOutput::PostAlonzo(d.decode_with(ctx)?)) + } + _ => Err(minicbor::decode::Error::message( + "invalid type for transaction output struct", + )), + } + } +} + +impl minicbor::Encode for PseudoTransactionOutput +where + T: minicbor::Encode, +{ + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + PseudoTransactionOutput::Legacy(x) => x.encode(e, ctx), + PseudoTransactionOutput::PostAlonzo(x) => x.encode(e, ctx), + } + } +} + +pub use crate::babbage::TransactionOutput; + +pub use crate::babbage::MintedTransactionOutput; + +pub use crate::babbage::PseudoPostAlonzoTransactionOutput; + +pub use crate::babbage::PostAlonzoTransactionOutput; + +pub use crate::babbage::MintedPostAlonzoTransactionOutput; + +pub use crate::alonzo::VKeyWitness; + +pub use crate::alonzo::NativeScript; + +pub use crate::alonzo::PlutusScript as PlutusV1Script; + +pub use crate::babbage::PlutusV2Script; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(transparent)] +pub struct PlutusV3Script(#[n(0)] pub Bytes); + +impl AsRef<[u8]> for PlutusV3Script { + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + +pub use crate::alonzo::BigInt; + +pub use crate::alonzo::PlutusData; + +pub use crate::alonzo::Constr; + +pub use crate::alonzo::ExUnits; + +pub use crate::alonzo::ExUnitPrices; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +#[cbor(index_only)] +pub enum RedeemerTag { + #[n(0)] + Spend, + #[n(1)] + Mint, + #[n(2)] + Cert, + #[n(3)] + Reward, + #[n(4)] + DRep, + #[n(5)] + VotingProposal, +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +pub struct Redeemer { + #[n(0)] + pub tag: RedeemerTag, + + #[n(1)] + pub index: u32, + + #[n(2)] + pub data: PlutusData, + + #[n(3)] + pub ex_units: ExUnits, +} + +pub use crate::alonzo::BootstrapWitness; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] +#[cbor(map)] +pub struct WitnessSet { + #[n(0)] + pub vkeywitness: Option>, + + #[n(1)] + pub native_script: Option>, + + #[n(2)] + pub bootstrap_witness: Option>, + + #[n(3)] + pub plutus_v1_script: Option>, + + #[n(4)] + pub plutus_data: Option>, + + #[n(5)] + pub redeemer: Option>, + + #[n(6)] + pub plutus_v2_script: Option>, + + #[n(7)] + pub plutus_v3_script: Option>, +} + +#[derive(Encode, Decode, Debug, PartialEq, Clone)] +#[cbor(map)] +pub struct MintedWitnessSet<'b> { + #[n(0)] + pub vkeywitness: Option>, + + #[n(1)] + pub native_script: Option>, + + #[n(2)] + pub bootstrap_witness: Option>, + + #[n(3)] + pub plutus_v1_script: Option>, + + #[b(4)] + pub plutus_data: Option>>, + + #[n(5)] + pub redeemer: Option>, + + #[n(6)] + pub plutus_v2_script: Option>, + + #[n(7)] + pub plutus_v3_script: Option>, +} + +impl<'b> From> for WitnessSet { + fn from(x: MintedWitnessSet<'b>) -> Self { + WitnessSet { + vkeywitness: x.vkeywitness, + native_script: x.native_script, + bootstrap_witness: x.bootstrap_witness, + plutus_v1_script: x.plutus_v1_script, + plutus_data: x + .plutus_data + .map(|x| x.into_iter().map(|x| x.unwrap()).collect()), + redeemer: x.redeemer, + plutus_v2_script: x.plutus_v2_script, + plutus_v3_script: x.plutus_v3_script, + } + } +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)] +#[cbor(map)] +pub struct PostAlonzoAuxiliaryData { + #[n(0)] + pub metadata: Option, + + #[n(1)] + pub native_scripts: Option>, + + #[n(2)] + pub plutus_v1_scripts: Option>, + + #[n(3)] + pub plutus_v2_scripts: Option>, + + #[n(4)] + pub plutus_v3_scripts: Option>, +} + +pub type DatumHash = Hash<32>; + +//pub type Data = CborWrap; + +// datum_option = [ 0, $hash32 // 1, data ] +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum PseudoDatumOption { + Hash(Hash<32>), + Data(CborWrap), +} + +impl<'b, C, T> minicbor::Decode<'b, C> for PseudoDatumOption +where + T: minicbor::Decode<'b, C>, +{ + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + match d.u8()? { + 0 => Ok(Self::Hash(d.decode_with(ctx)?)), + 1 => Ok(Self::Data(d.decode_with(ctx)?)), + _ => Err(minicbor::decode::Error::message( + "invalid variant for datum option enum", + )), + } + } +} + +impl minicbor::Encode for PseudoDatumOption +where + T: minicbor::Encode, +{ + fn encode( + &self, + e: &mut minicbor::Encoder, + ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + match self { + Self::Hash(x) => e.encode_with((0, x), ctx)?, + Self::Data(x) => e.encode_with((1, x), ctx)?, + }; + + Ok(()) + } +} + +pub type DatumOption = PseudoDatumOption; + +pub type MintedDatumOption<'b> = PseudoDatumOption>; + +impl<'b> From> for DatumOption { + fn from(value: MintedDatumOption<'b>) -> Self { + match value { + PseudoDatumOption::Hash(x) => Self::Hash(x), + PseudoDatumOption::Data(x) => Self::Data(CborWrap(x.unwrap().unwrap())), + } + } +} + +// script_ref = #6.24(bytes .cbor script) +pub type ScriptRef = CborWrap