Skip to content

Commit a87de3c

Browse files
committed
feat: snapshot bootstrapper module
1 parent 68321e9 commit a87de3c

File tree

10 files changed

+371
-21
lines changed

10 files changed

+371
-21
lines changed

Cargo.toml

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,24 @@ members = [
66
"common",
77

88
# Modules
9-
"modules/genesis_bootstrapper", # Genesis bootstrap UTXOs
10-
"modules/mithril_snapshot_fetcher",# Mithril snapshot fetcher
11-
"modules/upstream_chain_fetcher", # Upstream chain fetcher
12-
"modules/block_unpacker", # Block to transaction unpacker
13-
"modules/tx_unpacker", # Tx to UTXO unpacker
14-
"modules/utxo_state", # UTXO state
15-
"modules/spo_state", # SPO state
16-
"modules/drep_state", # DRep state
17-
"modules/governance_state", # Governance state
18-
"modules/parameters_state", # Keeps track of protocol parameters
19-
"modules/stake_delta_filter", # Filters address deltas
20-
"modules/epoch_activity_counter", # Counts fees and block producers for rewards
21-
"modules/accounts_state", # Tracks stake and reward accounts
9+
"modules/genesis_bootstrapper", # Genesis bootstrap UTXOs
10+
"modules/mithril_snapshot_fetcher", # Mithril snapshot fetcher
11+
"modules/snapshot_bootstrapper", # Bootstrap state from a ledger snapshot
12+
"modules/upstream_chain_fetcher", # Upstream chain fetcher
13+
"modules/block_unpacker", # Block to transaction unpacker
14+
"modules/tx_unpacker", # Tx to UTXO unpacker
15+
"modules/utxo_state", # UTXO state
16+
"modules/spo_state", # SPO state
17+
"modules/drep_state", # DRep state
18+
"modules/parameters_state", # Keeps track of protocol parameters
19+
"modules/governance_state", # Governance state
20+
"modules/stake_delta_filter", # Filters address deltas
21+
"modules/epoch_activity_counter", # Counts fees and block producers for rewards
22+
"modules/accounts_state", # Tracks stake and reward accounts
2223

2324
# Process builds
24-
"processes/omnibus", # All-inclusive omnibus process
25-
"processes/replayer", # All-inclusive process to replay messages
25+
"processes/omnibus", # All-inclusive omnibus process
26+
"processes/replayer", # All-inclusive process to replay messages
2627
]
2728

2829
resolver = "2"

common/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ serde_json = "1.0"
2626
serde_with = { version = "3.12.0", features = ["hex"] }
2727
tokio = { version = "1", features = ["full"] }
2828
tracing = "0.1.40"
29+
minicbor = { version = "0.26.0", features = ["std", "half", "derive"] }
30+
2931

3032
[lib]
3133
crate-type = ["rlib"]

common/src/ledger_state.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use crate::{KeyHash, PoolRegistration};
2+
use anyhow::{bail, Context, Result};
3+
use std::{collections::BTreeMap, fs, path::Path};
4+
5+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
6+
pub struct LedgerState {
7+
pub spo_state: SPOState,
8+
}
9+
10+
pub struct UTxOState {}
11+
12+
pub struct StakeDistributionState {}
13+
14+
pub struct AccountState {}
15+
16+
pub struct ParametersState {}
17+
18+
#[derive(
19+
Debug, Clone, serde::Serialize, serde::Deserialize, minicbor::Decode, minicbor::Encode, Default,
20+
)]
21+
pub struct SPOState {
22+
#[n(0)]
23+
pub pools: BTreeMap<KeyHash, PoolRegistration>,
24+
#[n(1)]
25+
pub retiring: BTreeMap<KeyHash, u64>,
26+
}
27+
28+
pub struct DRepState {}
29+
30+
pub struct ProposalState {}
31+
32+
pub struct VotingState {}
33+
34+
impl LedgerState {
35+
pub fn from_directory(directory_path: impl AsRef<Path>) -> Result<Self> {
36+
let directory_path = directory_path.as_ref();
37+
if !directory_path.exists() {
38+
bail!("directory does not exist: {:?}", directory_path);
39+
}
40+
41+
if !directory_path.is_dir() {
42+
bail!("path is not a directory: {:?}", directory_path);
43+
}
44+
45+
let mut ledger_state = Self::default();
46+
ledger_state
47+
.load_from_directory(directory_path)
48+
.with_context(|| {
49+
format!(
50+
"Failed to load ledger state from directory: {:?}",
51+
directory_path
52+
)
53+
})?;
54+
55+
Ok(ledger_state)
56+
}
57+
58+
fn load_from_directory(&mut self, directory_path: impl AsRef<Path>) -> Result<()> {
59+
let directory_path = directory_path.as_ref();
60+
let entries = fs::read_dir(directory_path)
61+
.with_context(|| format!("failed to read directory: {:?}", directory_path))?;
62+
63+
for entry in entries {
64+
let entry = entry.with_context(|| "failed to read directory entry")?;
65+
let path = entry.path();
66+
67+
if path.is_file() && path.extension().map_or(false, |ext| ext == "cbor") {
68+
self.load_cbor_file(&path)
69+
.with_context(|| format!("failed to load CBOR file: {:?}", path))?;
70+
}
71+
}
72+
73+
Ok(())
74+
}
75+
76+
fn load_cbor_file(&mut self, file_path: impl AsRef<Path>) -> Result<()> {
77+
let file_path = file_path.as_ref();
78+
let filename = file_path
79+
.file_stem()
80+
.and_then(|s| s.to_str())
81+
.with_context(|| format!("invalid filename: {:?}", file_path))?;
82+
83+
let bytes =
84+
fs::read(file_path).with_context(|| format!("failed to read file: {:?}", file_path))?;
85+
86+
match filename {
87+
"pools" => {
88+
self.spo_state = minicbor::decode(&bytes)
89+
.with_context(|| format!("failed to decode SPO state from: {:?}", file_path))?;
90+
}
91+
_ => {
92+
// ignore unknown cbor files
93+
}
94+
}
95+
96+
Ok(())
97+
}
98+
}

common/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod address;
44
pub mod calculations;
55
pub mod cip19;
66
pub mod crypto;
7+
pub mod ledger_state;
78
pub mod messages;
89
pub mod params;
910
pub mod rational_number;

common/src/messages.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
// We don't use these messages in the acropolis_common crate itself
44
#![allow(dead_code)]
55

6+
use crate::ledger_state::SPOState;
7+
68
use crate::types::*;
79

810
// Caryatid core messages which we re-export
@@ -155,6 +157,11 @@ pub enum CardanoMessage {
155157
StakeAddressDeltas(StakeAddressDeltasMessage), // Stake part of address deltas
156158
}
157159

160+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
161+
pub enum SnapshotStateMessage {
162+
SPOState(SPOState),
163+
}
164+
158165
// === Global message enum ===
159166
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
160167
pub enum Message {
@@ -171,6 +178,9 @@ pub enum Message {
171178

172179
// Cardano messages with attached BlockInfo
173180
Cardano((BlockInfo, CardanoMessage)),
181+
182+
// Initialize state from a snapshot
183+
SnapshotState(SnapshotStateMessage),
174184
}
175185

176186
impl Default for Message {

common/src/types.rs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use anyhow::anyhow;
88
use bech32::{Bech32, Hrp};
99
use bitmask_enum::bitmask;
1010
use hex::decode;
11+
use minicbor::data::Tag;
1112
use serde::{Deserialize, Serialize};
1213
use serde_with::{hex::Hex, serde_as};
1314
use std::collections::{HashMap, HashSet};
@@ -149,6 +150,33 @@ pub struct Ratio {
149150
pub denominator: u64,
150151
}
151152

153+
// NOTE: Implementations from Pallas, why aren't we just using Pallas types here?
154+
impl<'b, C> minicbor::decode::Decode<'b, C> for Ratio {
155+
fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result<Self, minicbor::decode::Error> {
156+
// TODO: Enforce tag == 30 & array of size 2
157+
d.tag()?;
158+
d.array()?;
159+
Ok(Ratio {
160+
numerator: d.decode_with(ctx)?,
161+
denominator: d.decode_with(ctx)?,
162+
})
163+
}
164+
}
165+
166+
impl<C> minicbor::encode::Encode<C> for Ratio {
167+
fn encode<W: minicbor::encode::Write>(
168+
&self,
169+
e: &mut minicbor::Encoder<W>,
170+
ctx: &mut C,
171+
) -> Result<(), minicbor::encode::Error<W::Error>> {
172+
e.tag(Tag::new(30))?;
173+
e.array(2)?;
174+
e.encode_with(self.numerator, ctx)?;
175+
e.encode_with(self.denominator, ctx)?;
176+
Ok(())
177+
}
178+
}
179+
152180
/// General credential
153181
#[derive(
154182
Debug, Clone, Ord, Eq, PartialEq, PartialOrd, Hash, serde::Serialize, serde::Deserialize,
@@ -237,53 +265,129 @@ pub enum Relay {
237265
MultiHostName(MultiHostName),
238266
}
239267

268+
// NOTE: Implementations from Pallas, why aren't we just using Pallas types here?
269+
impl<'b, C> minicbor::decode::Decode<'b, C> for Relay {
270+
fn decode(d: &mut minicbor::Decoder<'b>, ctx: &mut C) -> Result<Self, minicbor::decode::Error> {
271+
d.array()?;
272+
let variant = d.u16()?;
273+
274+
match variant {
275+
0 => Ok(Relay::SingleHostAddr(SingleHostAddr {
276+
port: d.decode_with(ctx)?,
277+
ipv4: d.decode_with(ctx)?,
278+
ipv6: d.decode_with(ctx)?,
279+
})),
280+
1 => Ok(Relay::SingleHostName(SingleHostName {
281+
port: d.decode_with(ctx)?,
282+
dns_name: d.decode_with(ctx)?,
283+
})),
284+
2 => Ok(Relay::MultiHostName(MultiHostName {
285+
dns_name: d.decode_with(ctx)?,
286+
})),
287+
_ => Err(minicbor::decode::Error::message(
288+
"invalid variant id for Relay",
289+
)),
290+
}
291+
}
292+
}
293+
294+
impl<C> minicbor::encode::Encode<C> for Relay {
295+
fn encode<W: minicbor::encode::Write>(
296+
&self,
297+
e: &mut minicbor::Encoder<W>,
298+
ctx: &mut C,
299+
) -> Result<(), minicbor::encode::Error<W::Error>> {
300+
match self {
301+
Relay::SingleHostAddr(SingleHostAddr { port, ipv4, ipv6 }) => {
302+
e.array(4)?;
303+
e.encode_with(0, ctx)?;
304+
e.encode_with(port, ctx)?;
305+
e.encode_with(ipv4, ctx)?;
306+
e.encode_with(ipv6, ctx)?;
307+
308+
Ok(())
309+
}
310+
Relay::SingleHostName(SingleHostName { port, dns_name }) => {
311+
e.array(3)?;
312+
e.encode_with(1, ctx)?;
313+
e.encode_with(port, ctx)?;
314+
e.encode_with(dns_name, ctx)?;
315+
316+
Ok(())
317+
}
318+
Relay::MultiHostName(MultiHostName { dns_name }) => {
319+
e.array(2)?;
320+
e.encode_with(2, ctx)?;
321+
e.encode_with(dns_name, ctx)?;
322+
323+
Ok(())
324+
}
325+
}
326+
}
327+
}
328+
240329
/// Pool metadata
241330
#[serde_as]
242-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
331+
#[derive(
332+
Debug, Clone, serde::Serialize, serde::Deserialize, minicbor::Encode, minicbor::Decode,
333+
)]
243334
pub struct PoolMetadata {
244335
/// Metadata URL
336+
#[n(0)]
245337
pub url: String,
246338

247339
/// Metadata hash
248340
#[serde_as(as = "Hex")]
341+
#[n(1)]
249342
pub hash: DataHash,
250343
}
251344

252345
type RewardAccount = Vec<u8>;
253346

254347
/// Pool registration data
255348
#[serde_as]
256-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
349+
#[derive(
350+
Debug, Clone, serde::Serialize, serde::Deserialize, minicbor::Decode, minicbor::Encode,
351+
)]
257352
pub struct PoolRegistration {
258353
/// Operator pool key hash - used as ID
259354
#[serde_as(as = "Hex")]
355+
#[n(0)]
260356
pub operator: KeyHash,
261357

262358
/// VRF key hash
263359
#[serde_as(as = "Hex")]
360+
#[n(1)]
264361
pub vrf_key_hash: KeyHash,
265362

266363
/// Pledged Ada
364+
#[n(2)]
267365
pub pledge: Lovelace,
268366

269367
/// Fixed cost
368+
#[n(3)]
270369
pub cost: Lovelace,
271370

272371
/// Marginal cost (fraction)
372+
#[n(4)]
273373
pub margin: Ratio,
274374

275375
/// Reward account
276376
#[serde_as(as = "Hex")]
377+
#[n(5)]
277378
pub reward_account: Vec<u8>,
278379

279380
/// Pool owners by their key hash
280381
#[serde_as(as = "Vec<Hex>")]
382+
#[n(6)]
281383
pub pool_owners: Vec<KeyHash>,
282384

283385
// Relays
386+
#[n(7)]
284387
pub relays: Vec<Relay>,
285388

286389
// Metadata
390+
#[n(8)]
287391
pub pool_metadata: Option<PoolMetadata>,
288392
}
289393

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Acropolis snapshot bootstrapper module
2+
3+
[package]
4+
name = "acropolis_module_snapshot_bootstrapper"
5+
version = "0.1.0"
6+
edition = "2021"
7+
authors = ["Josh Marchand <[email protected]>"]
8+
description = "Snapshot bootstrapper Caryatid module for Acropolis"
9+
license = "Apache-2.0"
10+
11+
[dependencies]
12+
caryatid_sdk = "0.9.0"
13+
acropolis_common = { path = "../../common" }
14+
hex = "0.4"
15+
fraction = "0.15"
16+
pallas = "0.32.0"
17+
anyhow = "1.0"
18+
tokio = { version = "1", features = ["full"] }
19+
config = "0.15.11"
20+
tracing = "0.1.40"
21+
serde_json = "1.0.138"
22+
23+
[lib]
24+
path = "src/snapshot_bootstrapper.rs"

0 commit comments

Comments
 (0)