State machine utilities for reconstructing Solana blocks from Yellowstone Geyser streams.
This repository contains:
yellowstone-block-machine: the core sans-IO crate.examples/dragonsmouth: a runnable gRPC integration example.
Reconstructing full Solana blocks from Geyser is subtle because events may arrive out of order and slot lifecycle updates have edge-case behavior. This project centralizes those rules so consumers can:
- reconstruct complete blocks,
- detect forks and dead slots,
- process commitment updates with deterministic ordering.
crates/yellowstone-block-machine: library crate.examples/dragonsmouth: CLI example using Yellowstone gRPC.
Feature definitions live in crates/yellowstone-block-machine/Cargo.toml.
dragonsmouth-thin: enables the bundled Yellowstone proto adapter.dragonsmouth: enables client extensions on top ofdragonsmouth-thin.
Use the full integration:
[dependencies]
yellowstone-block-machine = { version = "0.8", features = ["dragonsmouth"] }Use only the proto adapter:
[dependencies]
yellowstone-block-machine = { version = "0.8", default-features = false, features = ["dragonsmouth-thin"] }From repo root:
cargo run -p dragonsmouth -- --config path/to/config.yaml --samples 10Config example:
endpoint: "https://your-yellowstone-endpoint"
x_token: "optional-auth-token"x-token is also accepted as an alias.
use futures_util::StreamExt;
use yellowstone_block_machine::dragonsmouth::client_ext::GeyserGrpcExt;
use yellowstone_block_machine::stream::BlockMachineOutput;
use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcBuilder};
use yellowstone_grpc_proto::geyser::{CommitmentLevel, SubscribeRequest};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut geyser = GeyserGrpcBuilder::from_shared("https://your-yellowstone-endpoint".into())?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.connect()
.await?;
let request = SubscribeRequest {
commitment: Some(CommitmentLevel::Confirmed as i32),
..Default::default()
};
let mut stream = geyser.subscribe_block(request).await?;
while let Some(item) = stream.next().await {
match item? {
BlockMachineOutput::FrozenBlock(block) => {
println!(
"slot={} tx={} accounts={} entries={}",
block.slot,
block.txn_len(),
block.account_len(),
block.entry_len(),
);
}
BlockMachineOutput::SlotCommitmentUpdate(update) => {
println!("slot={} commitment={:?}", update.slot, update.commitment);
}
BlockMachineOutput::ForkDetected(fork) => {
println!("fork detected: slot={}", fork.slot);
}
BlockMachineOutput::DeadBlockDetect(dead) => {
println!("dead block detected: slot={}", dead.slot);
}
}
}
Ok(())
}For a given slot:
FrozenBlockis emitted before any commitment update for that slot.- Commitment updates below the configured minimum are skipped.
- Fork/dead-slot outputs prune buffered data for the affected slot.
- https://docs.rs/yellowstone-block-machine/latest/yellowstone_block_machine/
crates/yellowstone-block-machine/README.md