Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,100 @@ jobs:
cargo install cargo-audit --quiet || true
cargo audit

# --------------------------------------------------------------------------
# Storage node on POSTGRES — the backend the fleet actually deploys.
#
# The `rust` job above runs the storage node with
# `--no-default-features --features local-dev,strict`, i.e. SQLite. That
# meant the node's three write-once economic registers — settlement slot,
# faucet ticket, economic root — had their non-equivocation properties
# asserted only against a backend nobody deploys. A safety property is not
# proven on a backend that never executed it, so this job runs the node's
# DEFAULT features (`strict,postgres`) against a real server.
#
# The property tests refuse to run without `DSM_TEST_DATABASE_URL` rather
# than skipping, so a job that lost its service container fails loudly
# instead of reporting a green board that executed nothing.
#
# NOT enabled here: the legacy `DSM_RUN_DB_TESTS=1` suites
# (device_api, b0x, bytecommit_chain). Those skip themselves by default and
# do not pass against Postgres today; resurrecting them is separate work and
# would make this board red for reasons unrelated to the registers.
# --------------------------------------------------------------------------
storage-node-postgres:
name: Storage Node (Postgres)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dsm_node_test
ports:
- 5432:5432
# Without a health check the job races the container's first accept().
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 20
env:
# TWO names, ONE database, and they are not redundant:
# DSM_TEST_DATABASE_URL is what the write-once register properties
# read. On a Postgres build its ABSENCE makes them fail rather than
# skip, which is the whole point of this job.
# DSM_DATABASE_URL is the node's own config variable, which
# `build_app_for_tests` and the integration suites read. On a Postgres
# build its default is a placeholder host that was never meant to
# resolve ("callers must supply DSM_DATABASE_URL"), so the suites that
# build a real app need it pointed at this container.
DSM_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/dsm_node_test
DSM_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/dsm_node_test
steps:
- uses: actions/checkout@v7

- uses: dtolnay/rust-toolchain@1.98.0

- uses: Swatinem/rust-cache@v2

- name: Install CI system deps
run: sudo apt-get update && sudo apt-get install -y protobuf-compiler postgresql-client

# The registers promise that an acknowledged claim survives a restart,
# and the node REFUSES to start on a server that cannot keep it. Show
# what this server reports so a future durability failure is diagnosable
# from the log rather than from a re-run.
- name: Report the server's durability posture
run: |
export PGPASSWORD=postgres
for setting in fsync full_page_writes synchronous_commit; do
printf '%s=%s\n' "$setting" \
"$(psql -h 127.0.0.1 -U postgres -d dsm_node_test -tAc "SHOW $setting")"
done

# --test-threads=1: every test shares ONE Postgres database, and
# `init_db`'s concurrent `CREATE TABLE IF NOT EXISTS` statements race
# each other on the system catalogs. Cells are keyed per test so tests
# never contend for one write-once row.
- name: Node board on Postgres
run: cargo test --locked -p dsm_storage_node --release -- --nocapture --test-threads=1

# A green board is not evidence that THESE tests ran: a renamed module
# or a dropped `#[cfg(test)]` would leave the board green and the
# registers unproven on the deployed backend. Count them.
- name: Prove the register properties executed on Postgres
run: |
set -o pipefail
cargo test --locked -p dsm_storage_node --release --lib -- --test-threads=1 \
db::write_once_properties db::pg::durable_posture_tests | tee registers.log
passed=$(sed -n 's/^test result: ok\. \([0-9]*\) passed.*/\1/p' registers.log | head -1)
echo "register/durability tests executed on Postgres: ${passed:-0}"
# May only grow. If a test is legitimately added, raise this floor in
# the same commit; if the count DROPS, the board stopped proving
# something it used to prove.
test "${passed:-0}" -ge 13

# --------------------------------------------------------------------------
# Formal validation: TLA+ model checking + real-code bridge harness
# --------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,16 +71,21 @@ pub trait PeerEvidenceFetcher {
faucet_id: &[u8; 32],
ticket_index: u64,
) -> Result<Option<Vec<u8>>, PeerLineageFailure>;
/// The quorum-agreed winner bytes for one settlement-slot cell
/// `(vault_id, parent_sequence)`, read against the vault's COMMITTED set
/// and counted at its committed quorum — never the fetcher's own fleet.
fn settlement_slot_cell(
/// What the vault's COMMITTED set establishes about one settlement-slot
/// cell, counted at its committed quorum — never the fetcher's own fleet.
///
/// Returns the observation itself, not an `Option` and not a `Result`: a
/// transport failure IS `Unavailable`, so there is no error channel a
/// caller could collapse and no `None` that could stand for four
/// different things. A caller that needs a narrower answer must match all
/// four arms and say what it does with each.
fn settlement_slot_observation(
&self,
vault_id: &[u8; 32],
parent_sequence: u64,
storage_set: &crate::ccb::StorageSetMembers,
quorum: u32,
) -> Result<Option<Vec<u8>>, PeerLineageFailure>;
) -> crate::economic::cell_observation::CellObservation;
/// Exact immutable bytes at `addr` under `namespace`.
fn immutable(
&self,
Expand Down Expand Up @@ -155,18 +160,15 @@ impl ProvenanceResolver for WalkingResolver<'_> {
.map(|envelope_bytes| FaucetTicketWin { envelope_bytes })
}

fn winning_settlement_slot_claim(
fn settlement_slot_observation(
&self,
vault_id: &[u8; 32],
parent_sequence: u64,
storage_set: &crate::ccb::StorageSetMembers,
quorum: u32,
) -> Option<crate::economic::provenance::SettlementSlotWin> {
) -> crate::economic::cell_observation::CellObservation {
self.fetcher
.settlement_slot_cell(vault_id, parent_sequence, storage_set, quorum)
.ok()
.flatten()
.map(|envelope_bytes| crate::economic::provenance::SettlementSlotWin { envelope_bytes })
.settlement_slot_observation(vault_id, parent_sequence, storage_set, quorum)
}

fn immutable_evidence(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,6 @@ pub struct FaucetTicketWin {
pub envelope_bytes: Vec<u8>,
}

/// The live-quorum answer for one settlement-slot cell: the exact v2 claim
/// envelope bytes a quorum of the vault's birth set holds as the winner.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SettlementSlotWin {
pub envelope_bytes: Vec<u8>,
}

/// Why a peer's lineage could not be resolved to a validated transition.
///
/// The taxonomy is load-bearing: a retry-able outage and an authenticated
Expand Down Expand Up @@ -197,17 +190,18 @@ pub trait ProvenanceResolver {
/// claims were written to. The caller must have required that quorum to be
/// canonical before calling.
///
/// `None` when no quorum-agreed winner exists for any reason — empty,
/// contested, or unreadable. The 0x0026 arm fails closed on all three,
/// which is why they are not distinguished here; a caller that must tell
/// them apart observes the cell directly.
fn winning_settlement_slot_claim(
/// Returns the OBSERVATION, not an `Option`. The four answers mean
/// different things and carry different verdicts — a divergence is a
/// quarantine, an outage is retryable, and neither is evidence that a
/// credit was forged — so there is deliberately no adapter here that
/// could turn `Conflict` into "no winner".
fn settlement_slot_observation(
&self,
vault_id: &[u8; 32],
parent_sequence: u64,
storage_set: &crate::ccb::StorageSetMembers,
quorum: u32,
) -> Option<SettlementSlotWin>;
) -> crate::economic::cell_observation::CellObservation;

/// Exact immutable bytes at `addr` under `namespace` — evidence the
/// verifier itself checks (the resolver supplies bytes, never verdicts,
Expand Down Expand Up @@ -1222,13 +1216,50 @@ pub fn verify_credit_source(
}
}
// ── 8. The quorum slot winner: exclusivity's liveness anchor ──
let win = resolver
.winning_settlement_slot_claim(&vault, *parent_sequence, &vn.storage_set, vn.quorum)
.ok_or_else(|| {
invalid("no quorum-agreed settlement-slot winner for this parent".into())
})?;
// EVERY ANSWER MEANS SOMETHING DIFFERENT, and the verdicts differ.
// The taxonomy this file states — an outage retries, a forgery does
// not — is only true if it is preserved here: a divergence in a
// write-once cell is a QUARANTINE, an unreadable cell is
// RETRYABLE, and neither is evidence that this credit was forged.
// Collapsing them into "no winner" reports a network fault as a
// forgery and a forgery as a network fault.
let envelope_bytes = match resolver.settlement_slot_observation(
&vault,
*parent_sequence,
&vn.storage_set,
vn.quorum,
) {
crate::economic::cell_observation::CellObservation::Claimed(bytes) => bytes,
crate::economic::cell_observation::CellObservation::Conflict { distinct } => {
return Err(ProvenanceError::OwnerLineage(
PeerLineageFailure::Quarantined(format!(
"the settlement-slot cell for this parent holds {distinct} \
contradictory claims"
)),
))
}
crate::economic::cell_observation::CellObservation::Unavailable {
attributed,
required,
} => {
return Err(ProvenanceError::OwnerLineage(
PeerLineageFailure::Incomplete(format!(
"only {attributed} of the vault's members answered the \
settlement-slot cell ({required} required)"
)),
))
}
// A quorum of members each said there is no claim here. That
// IS evidence, and it says this settle never won exclusivity
// over the parent it names.
crate::economic::cell_observation::CellObservation::EmptyAtQuorum => {
return Err(invalid(
"no settlement-slot claim was established for this parent".into(),
))
}
};
let claim = crate::dlv::settlement_slot_claim::decode_and_verify_settlement_slot_claim(
&win.envelope_bytes,
&envelope_bytes,
)
.map_err(|e| invalid(format!("slot winner: {e}")))?;
let vault_set_id = crate::ccb::storage_set_id(&vn.storage_set)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,14 +314,19 @@ impl ProvenanceResolver for OneTicket {
})
}

fn winning_settlement_slot_claim(
fn settlement_slot_observation(
&self,
_vault_id: &[u8; 32],
_parent_sequence: u64,
_storage_set: &dsm::ccb::StorageSetMembers,
_quorum: u32,
) -> Option<dsm::economic::provenance::SettlementSlotWin> {
None
) -> dsm::economic::cell_observation::CellObservation {
// This fixture roots no settlement slots: it cannot observe the
// cell, which is not the same as observing it empty.
dsm::economic::cell_observation::CellObservation::Unavailable {
attributed: 0,
required: 2,
}
}

fn immutable_evidence(
Expand Down Expand Up @@ -705,14 +710,19 @@ impl ProvenanceResolver for MarketRooted {
) -> Option<dsm::economic::provenance::FaucetTicketWin> {
None
}
fn winning_settlement_slot_claim(
fn settlement_slot_observation(
&self,
_vault_id: &[u8; 32],
_parent_sequence: u64,
_storage_set: &dsm::ccb::StorageSetMembers,
_quorum: u32,
) -> Option<dsm::economic::provenance::SettlementSlotWin> {
None
) -> dsm::economic::cell_observation::CellObservation {
// This fixture roots no settlement slots: it cannot observe the
// cell, which is not the same as observing it empty.
dsm::economic::cell_observation::CellObservation::Unavailable {
attributed: 0,
required: 2,
}
}
fn immutable_evidence(
&self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use dsm::crypto::sphincs::{generate_keypair, sphincs_sign, SphincsVariant};
use dsm::economic::issuance::IssuanceAuthorizationBody;
use dsm::economic::provenance::{
verify_transition_provenance, FaucetTicketWin, PeerLineageFailure, ProvenanceContext,
ProvenanceError, ProvenanceResolver, SettlementSlotWin, ValidatedPeerTransition,
ProvenanceError, ProvenanceResolver, ValidatedPeerTransition,
};
use dsm::economic::tree::EconomicSmt;
use dsm::economic::witness::EconomicTransitionWitness;
Expand Down Expand Up @@ -129,14 +129,19 @@ impl ProvenanceResolver for IssuanceResolver {
fn winning_faucet_ticket(&self, _f: &[u8; 32], _t: u64) -> Option<FaucetTicketWin> {
None
}
fn winning_settlement_slot_claim(
fn settlement_slot_observation(
&self,
_v: &[u8; 32],
_p: u64,
_s: &dsm::ccb::StorageSetMembers,
_q: u32,
) -> Option<SettlementSlotWin> {
None
) -> dsm::economic::cell_observation::CellObservation {
// This fixture roots no settlement slots: it cannot observe the
// cell, which is not the same as observing it empty.
dsm::economic::cell_observation::CellObservation::Unavailable {
attributed: 0,
required: 2,
}
}
fn immutable_evidence(
&self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::collections::BTreeMap;
use dsm::economic::mutation::EconomicLeafMutation;
use dsm::economic::provenance::{
verify_transition_provenance, FaucetTicketWin, PeerLineageFailure, ProvenanceContext,
ProvenanceError, ProvenanceResolver, SettlementSlotWin, ValidatedPeerTransition,
ProvenanceError, ProvenanceResolver, ValidatedPeerTransition,
};
use dsm::economic::state::{
EconomicBalanceState, EconomicLeafState, EconomicSettlementReceiptState,
Expand Down Expand Up @@ -233,14 +233,19 @@ impl ProvenanceResolver for ApplyResolver {
None
}

fn winning_settlement_slot_claim(
fn settlement_slot_observation(
&self,
_vault_id: &[u8; 32],
_parent_sequence: u64,
_storage_set: &dsm::ccb::StorageSetMembers,
_quorum: u32,
) -> Option<SettlementSlotWin> {
None
) -> dsm::economic::cell_observation::CellObservation {
// This fixture roots no settlement slots: it cannot observe the
// cell, which is not the same as observing it empty.
dsm::economic::cell_observation::CellObservation::Unavailable {
attributed: 0,
required: 2,
}
}

fn immutable_evidence(
Expand Down Expand Up @@ -490,14 +495,19 @@ fn an_unresolvable_trader_lineage_fails_closed() {
fn winning_faucet_ticket(&self, _f: &[u8; 32], _i: u64) -> Option<FaucetTicketWin> {
None
}
fn winning_settlement_slot_claim(
fn settlement_slot_observation(
&self,
_v: &[u8; 32],
_p: u64,
_storage_set: &dsm::ccb::StorageSetMembers,
_quorum: u32,
) -> Option<SettlementSlotWin> {
None
) -> dsm::economic::cell_observation::CellObservation {
// This fixture roots no settlement slots: it cannot observe the
// cell, which is not the same as observing it empty.
dsm::economic::cell_observation::CellObservation::Unavailable {
attributed: 0,
required: 2,
}
}
fn immutable_evidence(
&self,
Expand Down
Loading
Loading