diff --git a/Makefile b/Makefile index c202f79f..a5abd543 100644 --- a/Makefile +++ b/Makefile @@ -61,6 +61,9 @@ run: fmt: $(CARGO) fmt --all +check: + $(CARGO) fmt --all -- --check + clippy: $(CARGO) clippy --workspace -- -D warnings diff --git a/common/examples/test_streaming_parser.rs b/common/examples/test_streaming_parser.rs index e0932205..7109e914 100644 --- a/common/examples/test_streaming_parser.rs +++ b/common/examples/test_streaming_parser.rs @@ -2,6 +2,8 @@ // // Usage: cargo run --example test_streaming_parser --release -- +use acropolis_common::snapshot::protocol_parameters::ProtocolParameters; +use acropolis_common::snapshot::streaming_snapshot::GovernanceProtocolParametersCallback; use acropolis_common::snapshot::EpochCallback; use acropolis_common::snapshot::{ AccountState, DRepCallback, DRepInfo, GovernanceProposal, PoolCallback, PoolInfo, @@ -30,6 +32,9 @@ struct CountingCallbacks { sample_accounts: Vec, sample_dreps: Vec, sample_proposals: Vec, + gs_previous_params: Option, + gs_current_params: Option, + gs_future_params: Option, } impl UtxoCallback for CountingCallbacks { @@ -162,6 +167,115 @@ impl ProposalCallback for CountingCallbacks { } } +impl GovernanceProtocolParametersCallback for CountingCallbacks { + fn on_gs_protocol_parameters( + &mut self, + gs_previous_params: ProtocolParameters, + gs_current_params: ProtocolParameters, + gs_future_params: ProtocolParameters, + ) -> Result<()> { + eprintln!("\n=== Governance Protocol Parameters ===\n"); + + eprintln!("Previous Protocol Parameters:"); + eprintln!( + " Protocol Version: {}.{}", + gs_previous_params.protocol_version.major, gs_previous_params.protocol_version.minor + ); + eprintln!(" Min Fee A: {}", gs_previous_params.min_fee_a); + eprintln!(" Min Fee B: {}", gs_previous_params.min_fee_b); + eprintln!( + " Max Block Body Size: {}", + gs_previous_params.max_block_body_size + ); + eprintln!( + " Max Transaction Size: {}", + gs_previous_params.max_transaction_size + ); + eprintln!( + " Max Block Header Size: {}", + gs_previous_params.max_block_header_size + ); + eprintln!( + " Stake Pool Deposit: {}", + gs_previous_params.stake_pool_deposit + ); + eprintln!( + " Stake Credential Deposit: {}", + gs_previous_params.stake_credential_deposit + ); + eprintln!(" Min Pool Cost: {}", gs_previous_params.min_pool_cost); + eprintln!( + " Monetary Expansion: {}/{}", + gs_previous_params.monetary_expansion_rate.numerator, + gs_previous_params.monetary_expansion_rate.denominator + ); + eprintln!( + " Treasury Expansion: {}/{}", + gs_previous_params.treasury_expansion_rate.numerator, + gs_previous_params.treasury_expansion_rate.denominator + ); + + eprintln!("\nCurrent Protocol Parameters:"); + eprintln!( + " Protocol Version: {}.{}", + gs_current_params.protocol_version.major, gs_current_params.protocol_version.minor + ); + eprintln!(" Min Fee A: {}", gs_current_params.min_fee_a); + eprintln!(" Min Fee B: {}", gs_current_params.min_fee_b); + eprintln!( + " Max Block Body Size: {}", + gs_current_params.max_block_body_size + ); + eprintln!( + " Max Transaction Size: {}", + gs_current_params.max_transaction_size + ); + eprintln!( + " Max Block Header Size: {}", + gs_current_params.max_block_header_size + ); + eprintln!( + " Stake Pool Deposit: {}", + gs_current_params.stake_pool_deposit + ); + eprintln!( + " Stake Credential Deposit: {}", + gs_current_params.stake_credential_deposit + ); + eprintln!(" Min Pool Cost: {}", gs_current_params.min_pool_cost); + eprintln!( + " Monetary Expansion: {}/{}", + gs_current_params.monetary_expansion_rate.numerator, + gs_current_params.monetary_expansion_rate.denominator + ); + eprintln!( + " Treasury Expansion: {}/{}", + gs_current_params.treasury_expansion_rate.numerator, + gs_current_params.treasury_expansion_rate.denominator + ); + + eprintln!("\nFuture Protocol Parameters:"); + eprintln!( + " Protocol Version: {}.{}", + gs_future_params.protocol_version.major, gs_future_params.protocol_version.minor + ); + eprintln!(" Min Fee A: {}", gs_future_params.min_fee_a); + eprintln!(" Min Fee B: {}", gs_future_params.min_fee_b); + eprintln!( + " Max Block Body Size: {}", + gs_future_params.max_block_body_size + ); + + // Store for later display + self.gs_previous_params = Some(gs_previous_params); + self.gs_current_params = Some(gs_current_params); + self.gs_future_params = Some(gs_future_params); + + eprintln!("\n=== End Protocol Parameters ===\n"); + Ok(()) + } +} + impl EpochCallback for CountingCallbacks { fn on_epoch(&mut self, data: EpochBootstrapData) -> Result<()> { info!( diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index dc6b6712..f63074f3 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -195,7 +195,7 @@ impl PraosParams { impl From<&ShelleyParams> for PraosParams { fn from(params: &ShelleyParams) -> Self { - let active_slots_coeff = params.active_slots_coeff; + let active_slots_coeff = ¶ms.active_slots_coeff; let security_param = params.security_param; let stability_window = (security_param as u64) * active_slots_coeff.denom() / active_slots_coeff.numer() * 3; @@ -204,7 +204,7 @@ impl From<&ShelleyParams> for PraosParams { Self { security_param, - active_slots_coeff, + active_slots_coeff: active_slots_coeff.clone(), epoch_length: params.epoch_length, max_kes_evolutions: params.max_kes_evolutions, max_lovelace_supply: params.max_lovelace_supply, diff --git a/common/src/rational_number.rs b/common/src/rational_number.rs index 5c825bfc..3699b696 100644 --- a/common/src/rational_number.rs +++ b/common/src/rational_number.rs @@ -1,18 +1,89 @@ use anyhow::{anyhow, Result}; use bigdecimal::BigDecimal; +use minicbor::Decode; +use num_rational::Ratio; use num_traits::ToPrimitive; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_with::{DeserializeAs, SerializeAs}; +use std::fmt; +use std::ops::Deref; use std::str::FromStr; -pub type RationalNumber = num_rational::Ratio; +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct RationalNumber(pub Ratio); pub fn rational_number_from_f32(f: f32) -> Result { RationalNumber::approximate_float_unsigned(f) .ok_or_else(|| anyhow!("Cannot convert {f} to Rational")) } +impl RationalNumber { + pub const fn new(numerator: u64, denominator: u64) -> Self { + RationalNumber(Ratio::new_raw(numerator, denominator)) + } + pub fn approximate_float_unsigned(f: f32) -> Option { + // Call the underlying Ratio function and map the result back to Self (RationalNumber) + Ratio::approximate_float_unsigned(f).map(RationalNumber) + } + pub fn from(numerator: u64, denominator: u64) -> Self { + RationalNumber(Ratio::new(numerator, denominator)) + } + pub const ZERO: RationalNumber = Self::new(0, 1); + pub const ONE: RationalNumber = Self::new(1, 1); +} + +// Implement Deref to automatically access Ratio's methods +impl Deref for RationalNumber { + type Target = Ratio; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for RationalNumber { + // The associated error type must implement Debug + type Err = num_rational::ParseRatioError; + + fn from_str(s: &str) -> Result { + // Delegate the parsing logic to the underlying Ratio implementation + Ratio::from_str(s).map(RationalNumber) + } +} + +impl fmt::Display for RationalNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Delegate the formatting task to the inner Ratio field (self.0) + write!(f, "{}", self.0) + } +} + +// Implement the required minicbor::Decode +impl<'a, C> Decode<'a, C> for RationalNumber { + fn decode( + d: &mut minicbor::Decoder<'a>, + _ctx: &mut C, + ) -> Result { + // Handle optional CBOR tag 30 for rationals (used in snapshots) + if matches!(d.datatype()?, minicbor::data::Type::Tag) { + d.tag()?; // consume the tag + } + + d.array()?; + let num: u64 = d.u64()?; + let den: u64 = d.u64()?; + + if den == 0 { + return Err(minicbor::decode::Error::message( + "Denominator cannot be zero", + )); + } + + Ok(RationalNumber(Ratio::new(num, den))) + } +} + #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug, Clone)] #[serde(untagged)] pub enum ChameleonFraction { @@ -112,7 +183,7 @@ impl SerializeAs for ChameleonFraction { where S: Serializer, { - let ch = ChameleonFraction::from_rational(*src); + let ch = ChameleonFraction::from_rational(src.clone()); ch.serialize(serializer) } } diff --git a/common/src/snapshot/decode.rs b/common/src/snapshot/decode.rs new file mode 100644 index 00000000..7859db49 --- /dev/null +++ b/common/src/snapshot/decode.rs @@ -0,0 +1,665 @@ +// Copyright 2025 PRAGMA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use minicbor as cbor; +#[cfg(test)] +use std::fmt::Display; + +// Misc +// ---------------------------------------------------------------------------- + +pub fn decode_break<'d>( + d: &mut cbor::Decoder<'d>, + len: Option, +) -> Result { + if d.datatype()? == cbor::data::Type::Break { + // NOTE: If we encounter a rogue Break while decoding a definite map, that's an error. + if len.is_some() { + return Err(cbor::decode::Error::type_mismatch(cbor::data::Type::Break)); + } + + d.skip()?; + + return Ok(true); + } + + Ok(false) +} + +// Array +// ---------------------------------------------------------------------------- + +/// Decode any heterogeneous CBOR array, irrespective of whether they're indefinite or definite. +pub fn heterogeneous_array<'d, A>( + d: &mut cbor::Decoder<'d>, + elems: impl FnOnce( + &mut cbor::Decoder<'d>, + Box Result<(), cbor::decode::Error>>, + ) -> Result, +) -> Result { + let len = d.array()?; + + match len { + None => { + let result = elems(d, Box::new(|_| Ok(())))?; + decode_break(d, len)?; + Ok(result) + } + Some(len) => elems( + d, + Box::new(move |expected_len| { + if len != expected_len { + return Err(cbor::decode::Error::message(format!( + "CBOR array length mismatch: expected {expected_len} got {len}" + ))); + } + + Ok(()) + }), + ), + } +} + +// Map +// ---------------------------------------------------------------------------- + +/// Decode any heterogeneous CBOR map, irrespective of whether they're indefinite or definite. +/// +/// A good choice for `S` is generally to pick a tuple of `PartialDecoder<_>` for each field item +/// that needs decoding. For example: +/// +/// ```rs +/// let (address, value, datum, script) = decode_map( +/// d, +/// ( +/// missing_field::(0), +/// missing_field::(1), +/// with_default_value(MemoizedDatum::None), +/// with_default_value(None), +/// ), +/// |d| d.u8(), +/// |d, state, field| { +/// match field { +/// 0 => state.0 = decode_chunk(d, |d| decode_address(d.bytes()?)), +/// 1 => state.1 = decode_chunk(d, |d| d.decode()), +/// 2 => state.2 = decode_chunk(d, decode_datum), +/// 3 => state.3 = decode_chunk(d, decode_reference_script), +/// _ => return unexpected_field::(field), +/// } +/// Ok(()) +/// }, +/// )?; +/// ``` +#[cfg(test)] +pub fn heterogeneous_map( + d: &mut cbor::Decoder<'_>, + mut state: S, + decode_key: impl Fn(&mut cbor::Decoder<'_>) -> Result, + mut decode_value: impl FnMut(&mut cbor::Decoder<'_>, &mut S, K) -> Result<(), cbor::decode::Error>, +) -> Result { + let len = d.map()?; + + let mut n = 0; + while len.is_none() || Some(n) < len { + if decode_break(d, len)? { + break; + } + + let k = decode_key(d)?; + decode_value(d, &mut state, k)?; + + n += 1; + } + + Ok(state) +} + +// PartialDecoder +// ---------------------------------------------------------------------------- + +/// A decoder that is part of another larger one. This is particularly useful to decode map +/// key/value in an arbitrary order; while logically recomposing them in a readable order. +#[cfg(test)] +type PartialDecoder = Box Result>; + +/// Wrap a decoder as a `PartialDecoder`; this is mostly a convenient utility to avoid boilerplate. +#[cfg(test)] +pub fn decode_chunk( + d: &mut cbor::Decoder<'_>, + decode: impl FnOnce(&mut cbor::Decoder<'_>) -> Result, +) -> PartialDecoder { + // NOTE: It is crucial that this happens *outside* of the boxed closure, to ensure bytes are consumed + // when the closure is created; not when it is invoked! + let a = decode(d); + Box::new(|| a) +} + +/// Yield a `PartialDecoder` that fails with a comprehensible error message when an expected field +/// is missing from the map. +#[cfg(test)] +pub fn missing_field(field_tag: impl Display) -> PartialDecoder { + let msg = format!( + "missing <{}> at field .{field_tag} in <{}> CBOR map", + std::any::type_name::(), + std::any::type_name::(), + ); + Box::new(move || Err(cbor::decode::Error::message(msg))) +} + +/// Yield a `PartialDecoder` that always succeeds with the given default value. +#[cfg(test)] +pub fn with_default_value(default: A) -> PartialDecoder { + Box::new(move || Ok(default)) +} + +/// Yield a `Result<_, decode::Error>` that always fails with a comprehensible error message when a +/// map key is unexpected. +#[cfg(test)] +pub fn unexpected_field(field_tag: impl Display) -> Result { + Err(cbor::decode::Error::message(format!( + "unexpected field .{field_tag} in <{}> CBOR map", + std::any::type_name::(), + ))) +} + +// Tests +// ---------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt::Debug; + + // Test fixtures + #[derive(Debug, PartialEq, Eq, Clone, Copy)] + struct Foo { + field0: u64, + field1: u64, + } + + // Wrapper for encoding as definite-length array + struct AsDefinite(T); + + impl cbor::encode::Encode<()> for AsDefinite<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.array(2)?; + e.u64(self.0.field0)?; + e.u64(self.0.field1)?; + Ok(()) + } + } + + impl<'d, C> cbor::decode::Decode<'d, C> for AsDefinite { + fn decode(d: &mut cbor::Decoder<'d>, ctx: &mut C) -> Result { + let len = d.array()?; + if len != Some(2) { + return Err(cbor::decode::Error::message( + "expected definite array of length 2", + )); + } + Ok(AsDefinite(Foo { + field0: d.decode_with(ctx)?, + field1: d.decode_with(ctx)?, + })) + } + } + + // Wrapper for encoding as indefinite-length array + struct AsIndefinite(T); + + impl cbor::encode::Encode<()> for AsIndefinite<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.begin_array()?; + e.u64(self.0.field0)?; + e.u64(self.0.field1)?; + e.end()?; + Ok(()) + } + } + + impl<'d, C> cbor::decode::Decode<'d, C> for AsIndefinite { + fn decode(d: &mut cbor::Decoder<'d>, ctx: &mut C) -> Result { + let len = d.array()?; + if len.is_some() { + return Err(cbor::decode::Error::message("expected indefinite array")); + } + let field0 = d.decode_with(ctx)?; + let field1 = d.decode_with(ctx)?; + if d.datatype()? != cbor::data::Type::Break { + return Err(cbor::decode::Error::message("expected break")); + } + d.skip()?; + Ok(AsIndefinite(Foo { field0, field1 })) + } + } + + // Wrapper for encoding as map + struct AsMap(T); + + impl cbor::encode::Encode<()> for AsMap<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.map(2)?; + e.u8(0)?; + e.u64(self.0.field0)?; + e.u8(1)?; + e.u64(self.0.field1)?; + Ok(()) + } + } + + // Composed encoders + impl cbor::encode::Encode<()> for AsIndefinite> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.begin_map()?; + e.u8(0)?; + e.u64(self.0 .0.field0)?; + e.u8(1)?; + e.u64(self.0 .0.field1)?; + e.end()?; + Ok(()) + } + } + + impl cbor::encode::Encode<()> for AsDefinite> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.map(2)?; + e.u8(0)?; + e.u64(self.0 .0.field0)?; + e.u8(1)?; + e.u64(self.0 .0.field1)?; + Ok(()) + } + } + + // Helper functions + fn to_cbor cbor::encode::Encode<()>>(value: &T) -> Vec { + let mut buf = Vec::new(); + let mut encoder = cbor::Encoder::new(&mut buf); + encoder.encode(value).unwrap(); + buf + } + + fn from_cbor<'d, T: cbor::decode::Decode<'d, ()>>(bytes: &'d [u8]) -> Option { + cbor::decode(bytes).ok() + } + + fn from_cbor_no_leftovers<'d, T: cbor::decode::Decode<'d, ()>>( + bytes: &'d [u8], + ) -> Result { + let mut decoder = cbor::Decoder::new(bytes); + let result = decoder.decode()?; + if decoder.position() != bytes.len() { + return Err(cbor::decode::Error::message("leftover bytes")); + } + Ok(result) + } + + fn assert_ok cbor::decode::Decode<'d, ()>>(left: T, bytes: &[u8]) { + assert_eq!( + Ok(left), + from_cbor_no_leftovers::(bytes).map_err(|e| e.to_string()) + ); + } + + fn assert_err cbor::decode::Decode<'d, ()>>(msg: &str, bytes: &[u8]) { + match from_cbor_no_leftovers::(bytes).map_err(|e| e.to_string()) { + Err(e) => assert!(e.contains(msg), "{e}"), + Ok(ok) => panic!("expected error but got {:#?}", ok), + } + } + + const FIXTURE: Foo = Foo { + field0: 14, + field1: 42, + }; + + mod heterogeneous_array_tests { + use super::*; + + #[test] + fn happy_case() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // A flexible decoder that can ingest both definite and indefinite arrays. + impl<'d, C> cbor::decode::Decode<'d, C> for TestCase { + fn decode( + d: &mut cbor::Decoder<'d>, + ctx: &mut C, + ) -> Result { + heterogeneous_array(d, |d, assert_len| { + assert_len(2)?; + Ok(TestCase(Foo { + field0: d.decode_with(ctx)?, + field1: d.decode_with(ctx)?, + })) + }) + } + } + + assert_ok(TestCase(FIXTURE), &to_cbor(&AsDefinite(&FIXTURE))); + assert_ok(TestCase(FIXTURE), &to_cbor(&AsIndefinite(&FIXTURE))); + } + + #[test] + fn smaller_definite_length() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // A decoder which expects less elements than actually supplied. + impl<'d, C> cbor::decode::Decode<'d, C> for TestCase { + fn decode( + d: &mut cbor::Decoder<'d>, + ctx: &mut C, + ) -> Result { + heterogeneous_array(d, |d, assert_len| { + assert_len(1)?; + Ok(TestCase(Foo { + field0: d.decode_with(ctx)?, + field1: d.decode_with(ctx)?, + })) + }) + } + } + + assert_err::>("array length mismatch", &to_cbor(&AsDefinite(&FIXTURE))); + } + + #[test] + fn larger_definite_length() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // A decoder which expects more elements than actually supplied. + impl<'d, C> cbor::decode::Decode<'d, C> for TestCase { + fn decode( + d: &mut cbor::Decoder<'d>, + ctx: &mut C, + ) -> Result { + heterogeneous_array(d, |d, assert_len| { + assert_len(3)?; + Ok(TestCase(Foo { + field0: d.decode_with(ctx)?, + field1: d.decode_with(ctx)?, + })) + }) + } + } + + assert_err::>("array length mismatch", &to_cbor(&AsDefinite(&FIXTURE))) + } + + #[test] + fn incomplete_indefinite() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // An incomplete encoder, which skips the final break on indefinite arrays. + impl cbor::encode::Encode<()> for TestCase<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.begin_array()?; + e.u64(self.0.field0)?; + e.u64(self.0.field1)?; + Ok(()) + } + } + + let bytes = to_cbor(&TestCase(&FIXTURE)); + + assert!(from_cbor::>(&bytes).is_none()); + assert!(from_cbor::>(&bytes).is_none()); + } + } + + mod heterogeneous_map_tests { + use super::*; + + /// A decoder for `Foo` that interpret it as a map, and fails in case of a missing field. + #[derive(Debug, PartialEq, Eq)] + struct NoMissingFields(A); + impl<'d, C> cbor::decode::Decode<'d, C> for NoMissingFields { + fn decode( + d: &mut cbor::Decoder<'d>, + _ctx: &mut C, + ) -> Result { + let (field0, field1) = heterogeneous_map( + d, + (missing_field::(0), missing_field::(1)), + |d| d.u8(), + |d, state, field| { + match field { + 0 => state.0 = decode_chunk(d, |d| d.u64()), + 1 => state.1 = decode_chunk(d, |d| d.u64()), + _ => return unexpected_field::(field), + } + Ok(()) + }, + )?; + + Ok(NoMissingFields(Foo { + field0: field0()?, + field1: field1()?, + })) + } + } + + /// A decoder for `Foo` that interpret it as a map, but allows fields to be missing. + #[derive(Debug, PartialEq, Eq)] + struct WithDefaultValues(A); + impl<'d, C> cbor::decode::Decode<'d, C> for WithDefaultValues { + fn decode( + d: &mut cbor::Decoder<'d>, + _ctx: &mut C, + ) -> Result { + let (field0, field1) = heterogeneous_map( + d, + (with_default_value(14_u64), with_default_value(42_u64)), + |d| d.u8(), + |d, state, field| { + match field { + 0 => state.0 = decode_chunk(d, |d| d.u64()), + 1 => state.1 = decode_chunk(d, |d| d.u64()), + _ => return unexpected_field::(field), + } + Ok(()) + }, + )?; + + Ok(WithDefaultValues(Foo { + field0: field0()?, + field1: field1()?, + })) + } + } + + #[test] + fn no_optional_fields_no_missing_fields() { + assert_ok( + NoMissingFields(FIXTURE), + &to_cbor(&AsIndefinite(AsMap(&FIXTURE))), + ); + + assert_ok( + NoMissingFields(FIXTURE), + &to_cbor(&AsDefinite(AsMap(&FIXTURE))), + ); + } + + #[test] + fn out_of_order_fields() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // An invalid encoder, which adds an extra break in an definite map. + impl cbor::encode::Encode<()> for TestCase<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.map(2)?; + e.u8(1)?; + e.u64(self.0.field1)?; + e.u8(0)?; + e.u64(self.0.field0)?; + Ok(()) + } + } + + assert_ok(NoMissingFields(FIXTURE), &to_cbor(&TestCase(&FIXTURE))); + } + + #[test] + fn optional_fields_no_missing_fields() { + assert_ok( + WithDefaultValues(FIXTURE), + &to_cbor(&AsIndefinite(AsMap(&FIXTURE))), + ); + + assert_ok( + WithDefaultValues(FIXTURE), + &to_cbor(&AsDefinite(AsMap(&FIXTURE))), + ); + } + + #[test] + fn one_field_missing() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + impl cbor::encode::Encode<()> for TestCase> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.map(1)?; + e.u8(0)?; + e.u64(self.0 .0.field0)?; + Ok(()) + } + } + + impl cbor::encode::Encode<()> for TestCase> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.begin_map()?; + e.u8(1)?; + e.u64(self.0 .0.field1)?; + e.end()?; + Ok(()) + } + } + + assert_err::>( + "missing at field .1", + &to_cbor(&TestCase(AsIndefinite(&FIXTURE))), + ); + + assert_ok( + WithDefaultValues(FIXTURE), + &to_cbor(&TestCase(AsIndefinite(&FIXTURE))), + ); + + assert_err::>( + "missing at field .0", + &to_cbor(&TestCase(AsDefinite(&FIXTURE))), + ); + + assert_ok( + WithDefaultValues(FIXTURE), + &to_cbor(&TestCase(AsDefinite(&FIXTURE))), + ); + } + + #[test] + fn rogue_break() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // An invalid encoder, which adds an extra break in an definite map. + impl cbor::encode::Encode<()> for TestCase<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.map(2)?; + e.u8(0)?; + e.u64(self.0.field0)?; + e.end()?; + Ok(()) + } + } + + assert_err::>( + "unexpected type break", + &to_cbor(&TestCase(&FIXTURE)), + ); + } + + #[test] + fn unexpected_field_tag() { + #[derive(Debug, PartialEq, Eq)] + struct TestCase(A); + + // An invalid encoder, which adds an extra break in an definite map. + impl cbor::encode::Encode<()> for TestCase<&Foo> { + fn encode( + &self, + e: &mut cbor::Encoder, + _ctx: &mut (), + ) -> Result<(), cbor::encode::Error> { + e.map(2)?; + e.u8(0)?; + e.u64(self.0.field0)?; + e.u8(14)?; + e.u64(self.0.field0)?; + Ok(()) + } + } + + assert_err::>( + "unexpected field .14", + &to_cbor(&TestCase(&FIXTURE)), + ); + } + } +} diff --git a/common/src/snapshot/mod.rs b/common/src/snapshot/mod.rs index 148675b1..d835b7f3 100644 --- a/common/src/snapshot/mod.rs +++ b/common/src/snapshot/mod.rs @@ -10,10 +10,12 @@ //! - Error types (`error.rs`) // Submodules +mod decode; mod error; pub mod mark_set_go; mod parser; pub mod pool_params; +pub mod protocol_parameters; pub mod streaming_snapshot; // Re-export error types diff --git a/common/src/snapshot/protocol_parameters.rs b/common/src/snapshot/protocol_parameters.rs new file mode 100644 index 00000000..d104d1c1 --- /dev/null +++ b/common/src/snapshot/protocol_parameters.rs @@ -0,0 +1,420 @@ +use crate::rational_number::RationalNumber; +use crate::{protocol_params::ProtocolVersion, snapshot::streaming_snapshot::Epoch}; +pub use crate::{ + CostModel, CostModels, DRepVotingThresholds, ExUnitPrices, ExUnits, Lovelace, + PoolVotingThresholds, ProtocolParamUpdate, Ratio, +}; + +use crate::snapshot::decode::heterogeneous_array; +use minicbor::{data::Tag, Decoder}; + +/// Model from https://github.com/IntersectMBO/formal-ledger-specifications/blob/master/src/Ledger/PParams.lagda +/// Some of the names have been adapted to improve readability. +/// Also see https://github.com/IntersectMBO/cardano-ledger/blob/d90eb4df4651970972d860e95f1a3697a3de8977/eras/conway/impl/cddl-files/conway.cddl#L324 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ProtocolParameters { + // Outside of all groups. + pub protocol_version: ProtocolVersion, + + // Network group + pub max_block_body_size: u64, + pub max_transaction_size: u64, + pub max_block_header_size: u16, + pub max_tx_ex_units: ExUnits, + pub max_block_ex_units: ExUnits, + pub max_value_size: u64, + pub max_collateral_inputs: u16, + + // Economic group + pub min_fee_a: Lovelace, + pub min_fee_b: u64, + pub stake_credential_deposit: Lovelace, + pub stake_pool_deposit: Lovelace, + pub monetary_expansion_rate: Ratio, + pub treasury_expansion_rate: Ratio, + pub min_pool_cost: u64, + pub lovelace_per_utxo_byte: Lovelace, + pub prices: ExUnitPrices, + pub min_fee_ref_script_lovelace_per_byte: Ratio, + pub max_ref_script_size_per_tx: u32, + pub max_ref_script_size_per_block: u32, + pub ref_script_cost_stride: u32, + pub ref_script_cost_multiplier: Ratio, + + // Technical group + pub stake_pool_max_retirement_epoch: Epoch, + pub optimal_stake_pools_count: u16, + pub pledge_influence: Ratio, + pub collateral_percentage: u16, + pub cost_models: CostModels, + + // Governance group + pub pool_voting_thresholds: PoolVotingThresholds, + pub drep_voting_thresholds: DRepVotingThresholds, + pub min_committee_size: u16, + pub max_committee_term_length: Epoch, + pub gov_action_lifetime: Epoch, + pub gov_action_deposit: Lovelace, + pub drep_deposit: Lovelace, + pub drep_expiry: Epoch, +} + +fn allow_tag(d: &mut Decoder<'_>, expected: Tag) -> Result<(), minicbor::decode::Error> { + if d.datatype()? == minicbor::data::Type::Tag { + let tag = d.tag()?; + if tag != expected { + return Err(minicbor::decode::Error::message(format!( + "invalid CBOR tag: expected {expected} got {tag}" + ))); + } + } + + Ok(()) +} + +fn decode_rationale(d: &mut Decoder<'_>) -> Result { + allow_tag(d, Tag::new(30))?; + heterogeneous_array(d, |d, assert_len| { + assert_len(2)?; + let numerator = d.u64()?; + let denominator = d.u64()?; + Ok(Ratio { + numerator, + denominator, + }) + }) +} + +impl Default for ProtocolParameters { + fn default() -> Self { + ProtocolParameters { + protocol_version: ProtocolVersion { major: 0, minor: 0 }, + min_fee_a: 0, + min_fee_b: 0, + max_block_body_size: 0, + max_transaction_size: 0, + max_block_header_size: 0, + stake_credential_deposit: 0, + stake_pool_deposit: 0, + stake_pool_max_retirement_epoch: 0, + optimal_stake_pools_count: 0, + pledge_influence: Ratio { + numerator: 0, + denominator: 1, + }, + monetary_expansion_rate: Ratio { + numerator: 0, + denominator: 1, + }, + treasury_expansion_rate: Ratio { + numerator: 0, + denominator: 1, + }, + min_pool_cost: 0, + lovelace_per_utxo_byte: 0, + cost_models: CostModels { + plutus_v1: None, + plutus_v2: None, + plutus_v3: None, + }, + prices: ExUnitPrices { + mem_price: RationalNumber::from(0, 1), + step_price: RationalNumber::from(0, 1), + }, + max_tx_ex_units: ExUnits { mem: 0, steps: 0 }, + max_block_ex_units: ExUnits { mem: 0, steps: 0 }, + max_value_size: 0, + collateral_percentage: 0, + max_collateral_inputs: 0, + pool_voting_thresholds: PoolVotingThresholds { + motion_no_confidence: RationalNumber::from(0, 1), + committee_normal: RationalNumber::from(0, 1), + committee_no_confidence: RationalNumber::from(0, 1), + hard_fork_initiation: RationalNumber::from(0, 1), + security_voting_threshold: RationalNumber::from(0, 1), + }, + drep_voting_thresholds: DRepVotingThresholds { + motion_no_confidence: RationalNumber::from(0, 1), + committee_normal: RationalNumber::from(0, 1), + committee_no_confidence: RationalNumber::from(0, 1), + update_constitution: RationalNumber::from(0, 1), + hard_fork_initiation: RationalNumber::from(0, 1), + pp_network_group: RationalNumber::from(0, 1), + pp_economic_group: RationalNumber::from(0, 1), + pp_technical_group: RationalNumber::from(0, 1), + pp_governance_group: RationalNumber::from(0, 1), + treasury_withdrawal: RationalNumber::from(0, 1), + }, + min_committee_size: 0, + max_committee_term_length: 0, + gov_action_lifetime: 0, + gov_action_deposit: 0, + drep_deposit: 0, + drep_expiry: 0, + min_fee_ref_script_lovelace_per_byte: Ratio { + numerator: 0, + denominator: 1, + }, + max_ref_script_size_per_tx: 0, + max_ref_script_size_per_block: 0, + ref_script_cost_stride: 0, + ref_script_cost_multiplier: Ratio { + numerator: 1, + denominator: 1, + }, + } + } +} + +fn decode_protocol_version( + d: &mut Decoder<'_>, +) -> Result { + heterogeneous_array(d, |d, assert_len| { + assert_len(2)?; + let major: u8 = d.u8()?; + + // See: https://github.com/IntersectMBO/cardano-ledger/blob/693218df6cd90263da24e6c2118bac420ceea3a1/eras/conway/impl/cddl-files/conway.cddl#L126 + if major > 12 { + return Err(minicbor::decode::Error::message( + "invalid protocol version's major: too high", + )); + } + Ok(ProtocolVersion { + major: major as u64, + minor: d.u64()?, + }) + }) +} + +impl<'b, C> minicbor::decode::Decode<'b, C> for ProtocolParameters { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; + + // Check first field - for future params it might be a variant tag + let first_field_type = d.datatype()?; + + // Peek at the value if it's U8 + if first_field_type == minicbor::data::Type::U8 { + let tag_value = d.u8()?; + + // future_pparams = [0] / [1, pparams_real] / [2, strict_maybe] + if tag_value == 0 { + // Return default/empty protocol parameters + return Ok(ProtocolParameters::default()); + } else if tag_value == 1 { + // Continue with normal parsing below (tag already consumed) + } else if tag_value == 2 { + // Next element might be Nothing or Just(params) + // For now, skip this case + return Err(minicbor::decode::Error::message( + "Future params variant [2] not yet implemented", + )); + } else { + // Not a variant tag, this is the actual first field (max_block_header_size?) + let unknown_field = tag_value; + return Self::decode_real_params(d, ctx, unknown_field); + } + } + + // If we get here, we have variant [1] or [2] and need to parse the real params + // For variant [1], the next field should be the start of pparams_real + // which starts with the first real field (not a tag) + let first_field_type = d.datatype()?; + if first_field_type == minicbor::data::Type::U8 { + let first_field = d.u8()?; + Self::decode_real_params(d, ctx, first_field) + } else { + Err(minicbor::decode::Error::message( + "Expected U8 for first field of pparams_real", + )) + } + } +} + +impl ProtocolParameters { + fn decode_real_params<'b, C>( + d: &mut minicbor::Decoder<'b>, + ctx: &mut C, + first_field: u8, + ) -> Result { + // first_field is field 0 which we already consumed (U8=44 or similar, unknown purpose) + + // Read what appears to be the fee parameters + let min_fee_a = d.u32()? as u64; + let min_fee_b = d.u32()? as u64; + + // Read what appears to be size limits (but check types - they might be u16 not u64) + let max_block_body_size = d.u16()? as u64; + let max_transaction_size = d.u16()? as u64; + + // Deposits + let stake_credential_deposit = d.u32()? as u64; + let stake_pool_deposit = d.u32()? as u64; + + // Retirement epoch + let stake_pool_max_retirement_epoch = d.u8()? as u64; + + // Pool count + let optimal_stake_pools_count = d.u16()?; + + // Fields 9-11 should be ratios (Tag 30) + let pledge_influence = decode_rationale(d)?; + let monetary_expansion_rate = decode_rationale(d)?; + let treasury_expansion_rate = decode_rationale(d)?; + + // Field 12 should be protocol version array + let protocol_version = decode_protocol_version(d)?; + + // Field 13 + let min_pool_cost = d.u32()? as u64; + + // Field 14 + let lovelace_per_utxo_byte = d.u16()? as u64; + + // Field 15: cost_models map - manually decode since CostModel format might be different + let mut plutus_v1 = None; + let mut plutus_v2 = None; + let mut plutus_v3 = None; + + let map_len = d.map()?; + + if let Some(len) = map_len { + for _ in 0..len { + let lang_id: u8 = d.decode()?; + + // Try decoding as array of i64 (could be indefinite) + let array_len = d.array()?; + + let mut costs = Vec::new(); + if array_len.is_none() { + // Indefinite array - read until break + loop { + match d.datatype()? { + minicbor::data::Type::Break => { + d.skip()?; // consume the break + break; + } + _ => { + // Decode as i64, handling different integer sizes + let cost: i64 = d.decode()?; + costs.push(cost); + } + } + } + } else if let Some(alen) = array_len { + for _ in 0..alen { + let cost: i64 = d.decode()?; + costs.push(cost); + } + } + + let cost_model = CostModel::new(costs); + match lang_id { + 0 => plutus_v1 = Some(cost_model), + 1 => plutus_v2 = Some(cost_model), + 2 => plutus_v3 = Some(cost_model), + _ => unreachable!("unexpected language version: {}", lang_id), + } + } + } + + // Field 16: prices - encoded as array containing two tag-30 ratios + d.array()?; // Outer array + let mem_price = decode_rationale(d)?; // First ratio (tag 30) + let step_price = decode_rationale(d)?; // Second ratio (tag 30) + let prices = ExUnitPrices { + mem_price: RationalNumber::from(mem_price.numerator, mem_price.denominator), + step_price: RationalNumber::from(step_price.numerator, step_price.denominator), + }; + + // Field 17: max_tx_ex_units + let max_tx_ex_units = d.decode_with(ctx)?; + + // Field 18: max_block_ex_units + let max_block_ex_units = d.decode_with(ctx)?; + + // Field 19: max_value_size + let max_value_size = d.u16()? as u64; + + // Field 20: collateral_percentage + let collateral_percentage = d.u16()?; + + // Field 21: max_collateral_inputs + let max_collateral_inputs = d.u16()?; + + // Field 22: pool_voting_thresholds + let pool_voting_thresholds = d.decode_with(ctx)?; + + // Field 23: drep_voting_thresholds + let drep_voting_thresholds = d.decode_with(ctx)?; + + // Field 24: min_committee_size + let min_committee_size = d.u16()?; + + // Field 25: max_committee_term_length + let max_committee_term_length = d.u64()?; + + // Field 26: gov_action_lifetime + let gov_action_lifetime = d.u64()?; + + // Field 27: gov_action_deposit + let gov_action_deposit = d.u64()?; + + // Field 28: drep_deposit + let drep_deposit = d.u64()?; + + // Field 29: drep_expiry + let drep_expiry = d.decode_with(ctx)?; + + // Field 30: min_fee_ref_script_lovelace_per_byte + let min_fee_ref_script_lovelace_per_byte = decode_rationale(d)?; + + // Field 0 (U8=44) - still unknown, need to determine max_block_header_size + let max_block_header_size = first_field as u16; + + Ok(ProtocolParameters { + protocol_version, + min_fee_a, + min_fee_b, + max_block_body_size, + max_transaction_size, + max_block_header_size, + stake_credential_deposit, + stake_pool_deposit, + stake_pool_max_retirement_epoch, + optimal_stake_pools_count, + pledge_influence, + monetary_expansion_rate, + treasury_expansion_rate, + min_pool_cost, + lovelace_per_utxo_byte, + cost_models: CostModels { + plutus_v1, + plutus_v2, + plutus_v3, + }, + prices, + max_tx_ex_units, + max_block_ex_units, + max_value_size, + collateral_percentage, + max_collateral_inputs, + pool_voting_thresholds, + drep_voting_thresholds, + min_committee_size, + max_committee_term_length, + gov_action_lifetime, + gov_action_deposit, + drep_deposit, + drep_expiry, + min_fee_ref_script_lovelace_per_byte, + max_ref_script_size_per_tx: 200 * 1024, + max_ref_script_size_per_block: 1024 * 1024, + ref_script_cost_stride: 25600, + ref_script_cost_multiplier: Ratio { + numerator: 12, + denominator: 10, + }, + }) + } +} diff --git a/common/src/snapshot/streaming_snapshot.rs b/common/src/snapshot/streaming_snapshot.rs index b1efe4bf..6bf68991 100644 --- a/common/src/snapshot/streaming_snapshot.rs +++ b/common/src/snapshot/streaming_snapshot.rs @@ -18,7 +18,7 @@ //! Parses CBOR dumps from Cardano Haskell node's GetCBOR ledger-state query. //! These snapshots represent the internal `NewEpochState` type and are not formally //! specified - see: https://github.com/IntersectMBO/cardano-ledger/blob/33e90ea03447b44a389985ca2b158568e5f4ad65/eras/shelley/impl/src/Cardano/Ledger/Shelley/LedgerState/Types.hs#L121-L131 -//! +//! and https://github.com/rrruko/nes-cddl-hs/blob/main/nes.cddl use anyhow::{anyhow, Context, Result}; use minicbor::data::Type; @@ -30,6 +30,7 @@ use std::io::{Read, Seek, SeekFrom}; use tracing::info; pub use crate::hash::Hash; +use crate::snapshot::protocol_parameters::ProtocolParameters; pub use crate::stake_addresses::{AccountState, StakeAddressState}; pub use crate::StakeCredential; @@ -312,7 +313,7 @@ impl<'b, C> minicbor::Decode<'b, C> for Account { pub use crate::types::AddrKeyhash; pub use crate::types::ScriptHash; -use crate::{EpochBootstrapData, PoolId}; +use crate::{Constitution, EpochBootstrapData, PoolId}; /// Alias minicbor as cbor for pool_params module pub use minicbor as cbor; @@ -816,12 +817,24 @@ pub trait ProposalCallback { fn on_proposals(&mut self, proposals: Vec) -> Result<()>; } +/// Callback invoked with Governance State ProtocolParameters (previous, current, future) +pub trait GovernanceProtocolParametersCallback { + /// Called once with all proposals + fn on_gs_protocol_parameters( + &mut self, + gs_previous_params: ProtocolParameters, + gs_current_params: ProtocolParameters, + gs_future_params: ProtocolParameters, + ) -> Result<()>; +} + /// Combined callback handler for all snapshot data pub trait SnapshotCallbacks: UtxoCallback + PoolCallback + StakeCallback + DRepCallback + + GovernanceProtocolParametersCallback + ProposalCallback + SnapshotsCallback + EpochCallback @@ -954,199 +967,229 @@ impl StreamingSnapshotParser { buffer }; - let mut decoder = Decoder::new(&metadata_buffer); - - // Navigate to NewEpochState root array - let new_epoch_state_len = decoder - .array() - .context("Failed to parse NewEpochState root array")? - .ok_or_else(|| anyhow!("NewEpochState must be a definite-length array"))?; - - if new_epoch_state_len < 4 { - return Err(anyhow!( + // Parse metadata using decoder - scope it to prevent accidental reuse + let ( + epoch, + blocks_previous_epoch, + blocks_current_epoch, + treasury, + reserves, + dreps, + pools, + accounts, + utxo_file_position, + ) = { + let mut decoder = Decoder::new(&metadata_buffer); + + // Navigate to NewEpochState root array + let new_epoch_state_len = decoder + .array() + .context("Failed to parse NewEpochState root array")? + .ok_or_else(|| anyhow!("NewEpochState must be a definite-length array"))?; + + if new_epoch_state_len < 4 { + return Err(anyhow!( "NewEpochState array too short: expected at least 4 elements, got {new_epoch_state_len}" )); - } + } - // Extract epoch number [0] - let epoch = decoder.u64().context("Failed to parse epoch number")?; + // Extract epoch number [0] + let epoch = decoder.u64().context("Failed to parse epoch number")?; - // Parse blocks_previous_epoch [1] and blocks_current_epoch [2] - let blocks_previous_epoch = - Self::parse_blocks_with_epoch(&mut decoder, epoch.saturating_sub(1)) - .context("Failed to parse blocks_previous_epoch")?; - let blocks_current_epoch = Self::parse_blocks_with_epoch(&mut decoder, epoch) - .context("Failed to parse blocks_current_epoch")?; + // Parse blocks_previous_epoch [1] and blocks_current_epoch [2] + let blocks_previous_epoch = + Self::parse_blocks_with_epoch(&mut decoder, epoch.saturating_sub(1)) + .context("Failed to parse blocks_previous_epoch")?; + let blocks_current_epoch = Self::parse_blocks_with_epoch(&mut decoder, epoch) + .context("Failed to parse blocks_current_epoch")?; - // Navigate to EpochState [3] - let epoch_state_len = decoder - .array() - .context("Failed to parse EpochState array")? - .ok_or_else(|| anyhow!("EpochState must be a definite-length array"))?; + // Navigate to EpochState [3] + let epoch_state_len = decoder + .array() + .context("Failed to parse EpochState array")? + .ok_or_else(|| anyhow!("EpochState must be a definite-length array"))?; - if epoch_state_len < 3 { - return Err(anyhow!( + if epoch_state_len < 3 { + return Err(anyhow!( "EpochState array too short: expected at least 3 elements, got {epoch_state_len}" )); - } + } - // Extract AccountState [3][0]: [treasury, reserves] - // Note: In Conway era, AccountState is just [treasury, reserves], not a full map - let account_state_len = decoder - .array() - .context("Failed to parse AccountState array")? - .ok_or_else(|| anyhow!("AccountState must be a definite-length array"))?; + // Extract AccountState [3][0]: [treasury, reserves] + // Note: In Conway era, AccountState is just [treasury, reserves], not a full map + let account_state_len = decoder + .array() + .context("Failed to parse AccountState array")? + .ok_or_else(|| anyhow!("AccountState must be a definite-length array"))?; - if account_state_len < 2 { - return Err(anyhow!( + if account_state_len < 2 { + return Err(anyhow!( "AccountState array too short: expected at least 2 elements, got {account_state_len}" )); - } - - // Parse treasury and reserves (can be negative in CBOR, so decode as i64 first) - let treasury_i64: i64 = decoder.decode().context("Failed to parse treasury")?; - let reserves_i64: i64 = decoder.decode().context("Failed to parse reserves")?; - let treasury = u64::try_from(treasury_i64).map_err(|_| anyhow!("treasury was negative"))?; - let reserves = u64::try_from(reserves_i64).map_err(|_| anyhow!("reserves was negative"))?; + } - // Skip any remaining AccountState fields - for i in 2..account_state_len { - decoder.skip().context(format!("Failed to skip AccountState[{i}]"))?; - } + // Parse treasury and reserves (can be negative in CBOR, so decode as i64 first) + let treasury_i64: i64 = decoder.decode().context("Failed to parse treasury")?; + let reserves_i64: i64 = decoder.decode().context("Failed to parse reserves")?; + let treasury = + u64::try_from(treasury_i64).map_err(|_| anyhow!("treasury was negative"))?; + let reserves = + u64::try_from(reserves_i64).map_err(|_| anyhow!("reserves was negative"))?; + + // Skip any remaining AccountState fields + for i in 2..account_state_len { + decoder.skip().context(format!("Failed to skip AccountState[{i}]"))?; + } - // Note: We defer the on_metadata callback until after we parse deposits from UTxOState[1] + // Note: We defer the on_metadata callback until after we parse deposits from UTxOState[1] - // Navigate to LedgerState [3][1] - let ledger_state_len = decoder - .array() - .context("Failed to parse LedgerState array")? - .ok_or_else(|| anyhow!("LedgerState must be a definite-length array"))?; + // Navigate to LedgerState [3][1] + let ledger_state_len = decoder + .array() + .context("Failed to parse LedgerState array")? + .ok_or_else(|| anyhow!("LedgerState must be a definite-length array"))?; - if ledger_state_len < 2 { - return Err(anyhow!( + if ledger_state_len < 2 { + return Err(anyhow!( "LedgerState array too short: expected at least 2 elements, got {ledger_state_len}" )); - } - - // Parse CertState [3][1][0] to extract DReps and pools - // CertState (ARRAY) - DReps, pools, accounts - // - [0] VotingState - DReps at [3][1][0][0][0] - // - [1] PoolState - pools at [3][1][0][1][0] - // - [2] DelegationState - accounts at [3][1][0][2][0][0] - // CertState = [VState, PState, DState] - let cert_state_len = decoder - .array() - .context("Failed to parse CertState array")? - .ok_or_else(|| anyhow!("CertState must be a definite-length array"))?; + } - if cert_state_len < 3 { - return Err(anyhow!( - "CertState array too short: expected at least 3 elements, got {cert_state_len}" - )); - } + // Parse CertState [3][1][0] to extract DReps and pools + // CertState (ARRAY) - DReps, pools, accounts + // - [0] VotingState - DReps at [3][1][0][0][0] + // - [1] PoolState - pools at [3][1][0][1][0] + // - [2] DelegationState - accounts at [3][1][0][2][0][0] + // CertState = [VState, PState, DState] + let cert_state_len = decoder + .array() + .context("Failed to parse CertState array")? + .ok_or_else(|| anyhow!("CertState must be a definite-length array"))?; + + if cert_state_len < 3 { + return Err(anyhow!( + "CertState array too short: expected at least 3 elements, got {cert_state_len}" + )); + } - // Parse VState [3][1][0][0] for DReps, which also skips committee_state and dormant_epoch. - // TODO: We may need to return to these later if we implement committee tracking. - let dreps = Self::parse_vstate(&mut decoder).context("Failed to parse VState for DReps")?; + // Parse VState [3][1][0][0] for DReps, which also skips committee_state and dormant_epoch. + // TODO: We may need to return to these later if we implement committee tracking. + let dreps = + Self::parse_vstate(&mut decoder).context("Failed to parse VState for DReps")?; - // Parse PState [3][1][0][1] for pools - let pools = Self::parse_pstate(&mut decoder).context("Failed to parse PState for pools")?; + // Parse PState [3][1][0][1] for pools + let pools = + Self::parse_pstate(&mut decoder).context("Failed to parse PState for pools")?; - // Parse DState [3][1][0][2] for accounts/delegations - // DState is an array: [unified_rewards, fut_gen_deleg, gen_deleg, instant_rewards] - decoder.array().context("Failed to parse DState array")?; + // Parse DState [3][1][0][2] for accounts/delegations + // DState is an array: [unified_rewards, fut_gen_deleg, gen_deleg, instant_rewards] + decoder.array().context("Failed to parse DState array")?; - // Parse unified rewards - it's actually an array containing the map - // UMap structure: [rewards_map, ...] - let umap_len = decoder.array().context("Failed to parse UMap array")?; + // Parse unified rewards - it's actually an array containing the map + // UMap structure: [rewards_map, ...] + let umap_len = decoder.array().context("Failed to parse UMap array")?; - // Parse the rewards map [0]: StakeCredential -> Account - let accounts_map: BTreeMap = decoder.decode()?; + // Parse the rewards map [0]: StakeCredential -> Account + let accounts_map: BTreeMap = decoder.decode()?; - // Skip remaining UMap elements if any - if let Some(len) = umap_len { - for _ in 1..len { - decoder.skip()?; + // Skip remaining UMap elements if any + if let Some(len) = umap_len { + for _ in 1..len { + decoder.skip()?; + } } - } - // Convert to AccountState for API - let accounts: Vec = accounts_map - .into_iter() - .map(|(credential, account)| { - // Convert StakeCredential to stake address representation - let stake_address = match &credential { - StakeCredential::AddrKeyHash(hash) => { - format!("stake_key_{}", hex::encode(hash)) - } - StakeCredential::ScriptHash(hash) => { - format!("stake_script_{}", hex::encode(hash)) - } - }; + // Convert to AccountState for API + let accounts: Vec = accounts_map + .into_iter() + .map(|(credential, account)| { + // Convert StakeCredential to stake address representation + let stake_address = match &credential { + StakeCredential::AddrKeyHash(hash) => { + format!("stake_key_{}", hex::encode(hash)) + } + StakeCredential::ScriptHash(hash) => { + format!("stake_script_{}", hex::encode(hash)) + } + }; - // Extract rewards from rewards_and_deposit (first element of tuple) - let rewards = match &account.rewards_and_deposit { - StrictMaybe::Just((reward, _deposit)) => *reward, - StrictMaybe::Nothing => 0, - }; + // Extract rewards from rewards_and_deposit (first element of tuple) + let rewards = match &account.rewards_and_deposit { + StrictMaybe::Just((reward, _deposit)) => *reward, + StrictMaybe::Nothing => 0, + }; - // Convert SPO delegation from StrictMaybe to Option - // PoolId is Hash<28>, we need to convert to Vec - let delegated_spo = match &account.pool { - StrictMaybe::Just(pool_id) => Some(*pool_id), - StrictMaybe::Nothing => None, - }; + // Convert SPO delegation from StrictMaybe to Option + // PoolId is Hash<28>, we need to convert to Vec + let delegated_spo = match &account.pool { + StrictMaybe::Just(pool_id) => Some(*pool_id), + StrictMaybe::Nothing => None, + }; - // Convert DRep delegation from StrictMaybe to Option - let delegated_drep = match &account.drep { - StrictMaybe::Just(drep) => Some(match drep { - DRep::Key(hash) => crate::DRepChoice::Key(*hash), - DRep::Script(hash) => crate::DRepChoice::Script(*hash), - DRep::Abstain => crate::DRepChoice::Abstain, - DRep::NoConfidence => crate::DRepChoice::NoConfidence, - }), - StrictMaybe::Nothing => None, - }; + // Convert DRep delegation from StrictMaybe to Option + let delegated_drep = match &account.drep { + StrictMaybe::Just(drep) => Some(match drep { + DRep::Key(hash) => crate::DRepChoice::Key(*hash), + DRep::Script(hash) => crate::DRepChoice::Script(*hash), + DRep::Abstain => crate::DRepChoice::Abstain, + DRep::NoConfidence => crate::DRepChoice::NoConfidence, + }), + StrictMaybe::Nothing => None, + }; - AccountState { - stake_address, - address_state: StakeAddressState { - registered: false, // Accounts are registered by SPOState - utxo_value: 0, // Not available in DState, would need to aggregate from UTxOs - rewards, - delegated_spo, - delegated_drep, - }, - } - }) - .collect(); + AccountState { + stake_address, + address_state: StakeAddressState { + registered: false, // Accounts are registered by SPOState + utxo_value: 0, // Not available in DState, would need to aggregate from UTxOs + rewards, + delegated_spo, + delegated_drep, + }, + } + }) + .collect(); - // Skip remaining DState fields (fut_gen_deleg, gen_deleg, instant_rewards) - // The UMap already handled all its internal elements including pointers + // Skip remaining DState fields (fut_gen_deleg, gen_deleg, instant_rewards) + // The UMap already handled all its internal elements including pointers - // Epoch State / Ledger State / Cert State / Delegation state / dsFutureGenDelegs - decoder.skip()?; + // Epoch State / Ledger State / Cert State / Delegation state / dsFutureGenDelegs + decoder.skip()?; - // Epoch State / Ledger State / Cert State / Delegation state / dsGenDelegs - decoder.skip()?; + // Epoch State / Ledger State / Cert State / Delegation state / dsGenDelegs + decoder.skip()?; - // Epoch State / Ledger State / Cert State / Delegation state / dsIRewards - decoder.skip()?; + // Epoch State / Ledger State / Cert State / Delegation state / dsIRewards + decoder.skip()?; - // Navigate to UTxOState [3][1][1] - let utxo_state_len = decoder - .array() - .context("Failed to parse UTxOState array")? - .ok_or_else(|| anyhow!("UTxOState must be a definite-length array"))?; + // Navigate to UTxOState [3][1][1] + let utxo_state_len = decoder + .array() + .context("Failed to parse UTxOState array")? + .ok_or_else(|| anyhow!("UTxOState must be a definite-length array"))?; - if utxo_state_len < 1 { - return Err(anyhow!( - "UTxOState array too short: expected at least 1 element, got {utxo_state_len}" - )); - } + if utxo_state_len < 1 { + return Err(anyhow!( + "UTxOState array too short: expected at least 1 element, got {utxo_state_len}" + )); + } - // Record the position before UTXO streaming - this is where UTXOs start in the file - let utxo_file_position = decoder.position() as u64; + // Record the position before UTXO streaming - this is where UTXOs start in the file + let utxo_file_position = decoder.position() as u64; + + // Return all the parsed metadata values + ( + epoch, + blocks_previous_epoch, + blocks_current_epoch, + treasury, + reserves, + dreps, + pools, + accounts, + utxo_file_position, + ) + }; // decoder goes out of scope here // Read only the UTXO section from the file (not the entire file!) let mut utxo_file = File::open(&self.file_path).context(format!( @@ -1265,14 +1308,20 @@ impl StreamingSnapshotParser { // skip ConstitutionalCommittee remainder_decoder.skip()?; - // skip Constitution - remainder_decoder.skip()?; + // Decode Constitution (unused currently, but serves as a "correctness" checkpoint while parsing) + let _constitution: Constitution = remainder_decoder.decode()?; + + // Governance State from epoch_state/ledger_state/utxo_state/gov_state + let gs_current_pparams: ProtocolParameters = remainder_decoder.decode()?; + let gs_previous_pparams: ProtocolParameters = remainder_decoder.decode()?; + let gs_future_pparams: ProtocolParameters = remainder_decoder.decode()?; // may be empty - // Current Protocol Params - remainder_decoder.skip()?; // Skip current protocol params instead of parsing + callbacks.on_gs_protocol_parameters( + gs_previous_pparams, + gs_current_pparams, + gs_future_pparams, + )?; - remainder_decoder.skip()?; // Previous Protocol Params - remainder_decoder.skip()?; // Future Protocol Params { remainder_decoder.array()?; // DRep Pulsing State remainder_decoder.array()?; // Pulsing Snapshot @@ -2191,7 +2240,11 @@ pub struct CollectingCallbacks { pub accounts: Vec, pub dreps: Vec, pub proposals: Vec, - epoch: EpochBootstrapData, + pub epoch: EpochBootstrapData, + pub snapshots: Option, + pub gs_protocol_previous_parameters: Option, + pub gs_protocol_current_parameters: Option, + pub gs_protocol_future_parameters: Option, } impl UtxoCallback for CollectingCallbacks { @@ -2236,6 +2289,20 @@ impl ProposalCallback for CollectingCallbacks { } } +impl GovernanceProtocolParametersCallback for CollectingCallbacks { + fn on_gs_protocol_parameters( + &mut self, + gs_previous_params: ProtocolParameters, + gs_current_params: ProtocolParameters, + gs_future_params: ProtocolParameters, + ) -> Result<()> { + self.gs_protocol_previous_parameters = Some(gs_previous_params); + self.gs_protocol_current_parameters = Some(gs_current_params); + self.gs_protocol_future_parameters = Some(gs_future_params); + Ok(()) + } +} + impl SnapshotCallbacks for CollectingCallbacks { fn on_metadata(&mut self, metadata: SnapshotMetadata) -> Result<()> { self.metadata = Some(metadata); diff --git a/common/src/types.rs b/common/src/types.rs index 9c451492..da996e42 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -820,7 +820,7 @@ impl Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Origin => write!(f, "origin"), - Self::Specific { hash, slot } => write!(f, "{}@{}", hash, slot), + Self::Specific { hash, slot } => write!(f, "{hash}@{slot}"), } } } @@ -1386,6 +1386,34 @@ pub struct Anchor { pub data_hash: DataHash, } +impl<'b, C> minicbor::Decode<'b, C> for Anchor { + fn decode( + d: &mut minicbor::Decoder<'b>, + _ctx: &mut C, + ) -> Result { + d.array()?; + + // URL can be either bytes or text string (snapshot format uses bytes) + let url = match d.datatype()? { + minicbor::data::Type::Bytes => { + let url_bytes = d.bytes()?; + String::from_utf8_lossy(url_bytes).to_string() + } + minicbor::data::Type::String => d.str()?.to_string(), + _ => { + return Err(minicbor::decode::Error::message( + "Expected bytes or string for Anchor URL", + )) + } + }; + + // data_hash is encoded as direct bytes, not an array + let data_hash = d.bytes()?.to_vec(); + + Ok(Self { url, data_hash }) + } +} + pub type DRepCredential = Credential; /// DRep Registration = reg_drep_cert @@ -1445,9 +1473,13 @@ pub struct ResignCommitteeCold { /// Governance actions data structures -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy)] +#[derive( + serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, Copy, minicbor::Decode, +)] pub struct ExUnits { + #[n(0)] pub mem: u64, + #[n(1)] pub steps: u64, } @@ -1457,6 +1489,30 @@ pub struct ExUnitPrices { pub step_price: RationalNumber, } +impl<'a, C> minicbor::Decode<'a, C> for ExUnitPrices { + fn decode( + d: &mut minicbor::Decoder<'a>, + _ctx: &mut C, + ) -> Result { + // Decode mem_price as [numerator, denominator] array + d.array()?; + let mem_num: u64 = d.decode()?; + let mem_den: u64 = d.decode()?; + let mem_price = RationalNumber::from(mem_num, mem_den); + + // Decode step_price as [numerator, denominator] array + d.array()?; + let step_num: u64 = d.decode()?; + let step_den: u64 = d.decode()?; + let step_price = RationalNumber::from(step_num, step_den); + + Ok(ExUnitPrices { + mem_price, + step_price, + }) + } +} + #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct GovActionId { pub transaction_id: TxHash, @@ -1519,8 +1575,8 @@ impl Display for GovActionId { } } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -pub struct CostModel(Vec); +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, minicbor::Decode)] +pub struct CostModel(#[n(0)] Vec); impl CostModel { pub fn new(m: Vec) -> Self { @@ -1539,26 +1595,41 @@ pub struct CostModels { pub plutus_v3: Option, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, minicbor::Decode)] pub struct PoolVotingThresholds { + #[n(0)] pub motion_no_confidence: RationalNumber, + #[n(1)] pub committee_normal: RationalNumber, + #[n(2)] pub committee_no_confidence: RationalNumber, + #[n(3)] pub hard_fork_initiation: RationalNumber, + #[n(4)] pub security_voting_threshold: RationalNumber, } -#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone)] +#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Eq, Clone, minicbor::Decode)] pub struct DRepVotingThresholds { + #[n(0)] pub motion_no_confidence: RationalNumber, + #[n(1)] pub committee_normal: RationalNumber, + #[n(2)] pub committee_no_confidence: RationalNumber, + #[n(3)] pub update_constitution: RationalNumber, + #[n(4)] pub hard_fork_initiation: RationalNumber, + #[n(5)] pub pp_network_group: RationalNumber, + #[n(6)] pub pp_economic_group: RationalNumber, + #[n(7)] pub pp_technical_group: RationalNumber, + #[n(8)] pub pp_governance_group: RationalNumber, + #[n(9)] pub treasury_withdrawal: RationalNumber, } @@ -1847,6 +1918,47 @@ pub struct Constitution { pub guardrail_script: Option, } +impl<'b, C> minicbor::Decode<'b, C> for Constitution { + fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result { + d.array()?; // Constitution array + + // In snapshot format, Anchor fields are flattened (not wrapped in array) + // Try to detect: if next element is bytes/string, it's flattened + // If next element is array, it's wrapped + let is_flattened = matches!( + d.datatype()?, + minicbor::data::Type::Bytes | minicbor::data::Type::String + ); + + let anchor = if is_flattened { + // Flattened format: [url, data_hash, guardrail_script] + let url = match d.datatype()? { + minicbor::data::Type::Bytes => { + let url_bytes = d.bytes()?; + String::from_utf8_lossy(url_bytes).to_string() + } + minicbor::data::Type::String => d.str()?.to_string(), + _ => { + return Err(minicbor::decode::Error::message( + "Expected bytes or string for Anchor URL", + )) + } + }; + let data_hash: Vec = d.bytes()?.to_vec(); + Anchor { url, data_hash } + } else { + // Wrapped format: [[url, data_hash], guardrail_script] + d.decode_with(ctx)? + }; + + let guardrail_script: Option = d.decode_with(ctx)?; + Ok(Self { + anchor, + guardrail_script, + }) + } +} + #[serde_as] #[derive(Serialize, PartialEq, Debug, Deserialize, Clone)] pub struct Committee { @@ -2425,7 +2537,7 @@ mod tests { make_committee_credential(false, 87), 1234, )]), - terms: RationalNumber::from(1), + terms: RationalNumber::ONE, }, }); diff --git a/modules/accounts_state/src/monetary.rs b/modules/accounts_state/src/monetary.rs index 76ad9584..7712683a 100644 --- a/modules/accounts_state/src/monetary.rs +++ b/modules/accounts_state/src/monetary.rs @@ -92,7 +92,7 @@ fn calculate_monetary_expansion( reserves: Lovelace, eta: &BigDecimal, ) -> BigDecimal { - let monetary_expansion_factor = params.protocol_params.monetary_expansion; + let monetary_expansion_factor = params.protocol_params.monetary_expansion.clone(); let monetary_expansion = (BigDecimal::from(reserves) * eta * BigDecimal::from(monetary_expansion_factor.numer()) / BigDecimal::from(monetary_expansion_factor.denom())) diff --git a/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs b/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs index ba03dc83..36001156 100644 --- a/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs +++ b/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs @@ -135,7 +135,7 @@ mod tests { fn test_lookup_in_overlay_schedule_1() { let genesis_values = GenesisValues::mainnet(); let genesis_delegs = genesis_values.genesis_delegs; - let decentralisation_param = RationalNumber::from(1); + let decentralisation_param = RationalNumber::ONE; let active_slots_coeff = RationalNumber::new(1, 20); let epoch_slot = 0; let obft_slot = lookup_in_overlay_schedule( diff --git a/modules/block_vrf_validator/src/ouroboros/tpraos.rs b/modules/block_vrf_validator/src/ouroboros/tpraos.rs index 05157e3b..b50c7d2d 100644 --- a/modules/block_vrf_validator/src/ouroboros/tpraos.rs +++ b/modules/block_vrf_validator/src/ouroboros/tpraos.rs @@ -201,7 +201,7 @@ mod tests { .unwrap(), ); let active_slots_coeff = RationalNumber::new(1, 20); - let decentralisation_param = RationalNumber::from(1); + let decentralisation_param = RationalNumber::ONE; let block_header_4490511: Vec = hex::decode(include_str!("./data/4490511.cbor")).unwrap(); diff --git a/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs b/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs index 79a51dd9..83df71db 100644 --- a/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs +++ b/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs @@ -183,7 +183,7 @@ pub fn validate_vrf_leader_value( Err(VrfLeaderValueTooBigError::VrfLeaderValueTooBig { pool_id: *pool_id, active_stake: *leader_relative_stake.numer(), - relative_stake: *leader_relative_stake, + relative_stake: leader_relative_stake.clone(), }) } } diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index f8d5f94d..2559b684 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -55,8 +55,8 @@ impl State { pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { if let Some(shelley_params) = msg.params.shelley.as_ref() { self.decentralisation_param = - Some(shelley_params.protocol_params.decentralisation_param); - self.active_slots_coeff = Some(shelley_params.active_slots_coeff); + Some(shelley_params.protocol_params.decentralisation_param.clone()); + self.active_slots_coeff = Some(shelley_params.active_slots_coeff.clone()); } } @@ -95,12 +95,12 @@ impl State { } }; - let Some(decentralisation_param) = self.decentralisation_param else { + let Some(decentralisation_param) = self.decentralisation_param.clone() else { return Err(Box::new(VrfValidationError::Other( "Decentralisation Param is not set".to_string(), ))); }; - let Some(active_slots_coeff) = self.active_slots_coeff else { + let Some(active_slots_coeff) = self.active_slots_coeff.clone() else { return Err(Box::new(VrfValidationError::Other( "Active Slots Coeff is not set".to_string(), ))); diff --git a/modules/governance_state/src/alonzo_babbage_voting.rs b/modules/governance_state/src/alonzo_babbage_voting.rs index f4992e5d..ec248776 100644 --- a/modules/governance_state/src/alonzo_babbage_voting.rs +++ b/modules/governance_state/src/alonzo_babbage_voting.rs @@ -282,7 +282,7 @@ mod tests { #[test] fn test_decentralisation_updates() -> Result<()> { - let dcu = extract_mainnet_parameter(|p| p.decentralisation_constant)?; + let dcu = extract_mainnet_parameter(|p| p.decentralisation_constant.clone())?; assert_eq!(DECENTRALISATION.len(), dcu.len()); for (decent, param) in DECENTRALISATION.iter().zip(dcu) { diff --git a/modules/governance_state/src/voting_state.rs b/modules/governance_state/src/voting_state.rs index 0d40a72e..c1f258b8 100644 --- a/modules/governance_state/src/voting_state.rs +++ b/modules/governance_state/src/voting_state.rs @@ -163,28 +163,46 @@ impl VotingRegistrationState { d_th = max(d_th, &d.pp_governance_group); } - VoteResult::new(c.threshold, *d_th, *p_th) - } - GovernanceAction::HardForkInitiation(_) => { - VoteResult::new(c.threshold, d.hard_fork_initiation, p.hard_fork_initiation) - } - GovernanceAction::TreasuryWithdrawals(_) => { - VoteResult::new(c.threshold, d.treasury_withdrawal, *zero) - } - GovernanceAction::NoConfidence(_) => { - VoteResult::new(*zero, d.motion_no_confidence, p.motion_no_confidence) + VoteResult::new(c.threshold.clone(), d_th.clone(), p_th.clone()) } + GovernanceAction::HardForkInitiation(_) => VoteResult::new( + c.threshold.clone(), + d.hard_fork_initiation.clone(), + p.hard_fork_initiation.clone(), + ), + GovernanceAction::TreasuryWithdrawals(_) => VoteResult::new( + c.threshold.clone(), + d.treasury_withdrawal.clone(), + zero.clone(), + ), + GovernanceAction::NoConfidence(_) => VoteResult::new( + zero.clone(), + d.motion_no_confidence.clone(), + p.motion_no_confidence.clone(), + ), GovernanceAction::UpdateCommittee(_) => { if thresholds.committee.is_empty() { - VoteResult::new(*zero, d.committee_no_confidence, p.committee_no_confidence) + VoteResult::new( + zero.clone(), + d.committee_no_confidence.clone(), + p.committee_no_confidence.clone(), + ) } else { - VoteResult::new(*zero, d.committee_normal, p.committee_normal) + VoteResult::new( + zero.clone(), + d.committee_normal.clone(), + p.committee_normal.clone(), + ) } } - GovernanceAction::NewConstitution(_) => { - VoteResult::new(c.threshold, d.update_constitution, *zero) + GovernanceAction::NewConstitution(_) => VoteResult::new( + c.threshold.clone(), + d.update_constitution.clone(), + zero.clone(), + ), + GovernanceAction::Information => { + VoteResult::new(zero.clone(), one.clone(), one.clone()) } - GovernanceAction::Information => VoteResult::new(*zero, *one, *one), } } diff --git a/modules/parameters_state/src/genesis_params.rs b/modules/parameters_state/src/genesis_params.rs index dfb6ee4f..6df1004b 100644 --- a/modules/parameters_state/src/genesis_params.rs +++ b/modules/parameters_state/src/genesis_params.rs @@ -156,6 +156,7 @@ fn map_conway(genesis: &conway::GenesisFile) -> Result { d_rep_activity: genesis.d_rep_activity, min_fee_ref_script_cost_per_byte: RationalNumber::from( genesis.min_fee_ref_script_cost_per_byte, + 1, ), plutus_v3_cost_model: CostModel::new(genesis.plutus_v3_cost_model.clone()), constitution: map_constitution(&genesis.constitution)?, diff --git a/modules/parameters_state/src/parameters_updater.rs b/modules/parameters_state/src/parameters_updater.rs index ca764ef9..b8c19b9f 100644 --- a/modules/parameters_state/src/parameters_updater.rs +++ b/modules/parameters_state/src/parameters_updater.rs @@ -231,7 +231,7 @@ impl ParametersUpdater { ); } } - c.threshold = cu.terms; + c.threshold = cu.terms.clone(); } fn apply_alonzo_babbage_outcome_elem(&mut self, u: &AlonzoBabbageVotingOutcome) -> Result<()> { diff --git a/modules/snapshot_bootstrapper/src/publisher.rs b/modules/snapshot_bootstrapper/src/publisher.rs index 4f5f05ef..2dbf3f45 100644 --- a/modules/snapshot_bootstrapper/src/publisher.rs +++ b/modules/snapshot_bootstrapper/src/publisher.rs @@ -1,4 +1,5 @@ use acropolis_common::protocol_params::{Nonces, PraosParams}; +use acropolis_common::snapshot::protocol_parameters::ProtocolParameters; use acropolis_common::snapshot::{RawSnapshotsContainer, SnapshotsCallback}; use acropolis_common::{ genesis_values::GenesisValues, @@ -7,9 +8,9 @@ use acropolis_common::{ }, params::EPOCH_LENGTH, snapshot::streaming_snapshot::{ - DRepCallback, DRepInfo, EpochCallback, GovernanceProposal, PoolCallback, PoolInfo, - ProposalCallback, SnapshotCallbacks, SnapshotMetadata, StakeCallback, UtxoCallback, - UtxoEntry, + DRepCallback, DRepInfo, EpochCallback, GovernanceProposal, + GovernanceProtocolParametersCallback, PoolCallback, PoolInfo, ProposalCallback, + SnapshotCallbacks, SnapshotMetadata, StakeCallback, UtxoCallback, UtxoEntry, }, stake_addresses::AccountState, BlockInfo, EpochBootstrapData, @@ -196,6 +197,23 @@ impl ProposalCallback for SnapshotPublisher { } } +impl GovernanceProtocolParametersCallback for SnapshotPublisher { + fn on_gs_protocol_parameters( + &mut self, + _gs_previous_params: ProtocolParameters, + _gs_current_params: ProtocolParameters, + _gs_future_params: ProtocolParameters, + ) -> Result<()> { + info!("Received governance protocol parameters (current, previous, future)"); + // TODO: Publish protocol parameters to appropriate message bus topics + // This could involve publishing messages for: + // - CurrentProtocolParameters → ParametersState processor + // - PreviousProtocolParameters → ParametersState processor + // - FutureProtocolParameters → ParametersState processor + Ok(()) + } +} + impl EpochCallback for SnapshotPublisher { fn on_epoch(&mut self, data: EpochBootstrapData) -> Result<()> { info!( diff --git a/modules/spo_state/src/epochs_history.rs b/modules/spo_state/src/epochs_history.rs index 6120ff77..bae59132 100644 --- a/modules/spo_state/src/epochs_history.rs +++ b/modules/spo_state/src/epochs_history.rs @@ -52,7 +52,7 @@ impl EpochState { epoch: self.epoch, blocks_minted: self.blocks_minted.unwrap_or(0), active_stake: self.active_stake.unwrap_or(0), - active_size: self.active_size.unwrap_or(RationalNumber::from(0)), + active_size: self.active_size.clone().unwrap_or(RationalNumber::ZERO), delegators_count: self.delegators_count.unwrap_or(0), pool_reward: self.pool_reward.unwrap_or(0), spo_reward: self.spo_reward.unwrap_or(0), diff --git a/modules/spo_state/src/spo_state.rs b/modules/spo_state/src/spo_state.rs index 44392dd4..c9a6912f 100644 --- a/modules/spo_state/src/spo_state.rs +++ b/modules/spo_state/src/spo_state.rs @@ -552,8 +552,8 @@ impl SPOState { .unwrap_or(0), active_size: epoch_state .as_ref() - .and_then(|state| state.active_size) - .unwrap_or(RationalNumber::from(0)), + .and_then(|state| state.active_size.clone()) + .unwrap_or(RationalNumber::ZERO), }) } else { PoolsStateQueryResponse::Error(QueryError::storage_disabled(