holdings: a member of a Space publishes what they hold as room-salted album keys, sealed to the room, so "who has this" is answered while they are offline

This commit is contained in:
KeelrLeek committed 2026-09-08 12:10:49 -05:00
1 parent 08ca09586a
commit a98c750908
12 files changed
+2955 -8

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
//! The SoulStream holdings contract. Single writer: one member's latest
//! signed album list in one Space, at the address (space id, member key).
//! Merge is the pointer contract's total order, (sequence, blake3) —
//! `persona_state::holdings::merge` decides, so every delivery order
//! converges, and a stale replay cannot bring a withdrawn list back.
//!
//! The body is opaque here: sealed to the room's epoch key in a private
//! Space, and the contract only ever checks the envelope and its signature.
//!
//! Build: `cargo build --release --target wasm32-unknown-unknown` in this
//! directory; copy `target/wasm32-unknown-unknown/release/
//! soulstream_holdings_contract.wasm` to `src-tauri/assets/holdings_contract.wasm`
//! and re-pin the code hash in `src-tauri/src/holdings.rs`.
use freenet_stdlib::prelude::*;
use persona_state::holdings as t;
pub struct Contract;
fn parts(parameters: &Parameters<'static>) -> Result<([u8; 16], [u8; 32]), ContractError> {
t::params_parts(parameters.as_ref())
.ok_or_else(|| ContractError::Other("params are not family+space+member".into()))
}
fn summary_of(bytes: &[u8], s: &t::HoldingsState) -> Vec<u8> {
let (seq, hash) = t::rank(bytes, s);
let mut out = Vec::with_capacity(40);
out.extend_from_slice(&seq.to_le_bytes());
out.extend_from_slice(&hash);
out
}
#[contract]
impl ContractInterface for Contract {
fn validate_state(
parameters: Parameters<'static>,
state: State<'static>,
_related: RelatedContracts<'static>,
) -> Result<ValidateResult, ContractError> {
let (space_id, member) = parts(&parameters)?;
if state.as_ref().is_empty() {
return Ok(ValidateResult::Valid);
}
match t::validate(&space_id, &member, state.as_ref()) {
Ok(_) => Ok(ValidateResult::Valid),
Err(e) => Err(ContractError::Other(format!("invalid holdings: {e}"))),
}
}
fn update_state(
parameters: Parameters<'static>,
state: State<'static>,
data: Vec<UpdateData<'static>>,
) -> Result<UpdateModification<'static>, ContractError> {
let (space_id, member) = parts(&parameters)?;
let mut candidates: Vec<Vec<u8>> = Vec::new();
if !state.as_ref().is_empty() {
candidates.push(state.as_ref().to_vec());
}
let mut incoming_valid = 0usize;
for ud in &data {
let mut push = |bytes: &[u8]| {
if bytes.is_empty() {
return;
}
if t::validate(&space_id, &member, bytes).is_ok() {
incoming_valid += 1;
}
candidates.push(bytes.to_vec());
};
match ud {
UpdateData::State(s) => push(s.as_ref()),
UpdateData::Delta(d) => push(d.as_ref()),
UpdateData::StateAndDelta { state, delta } => {
push(state.as_ref());
push(delta.as_ref());
}
_ => {}
}
}
if incoming_valid == 0 && !data.is_empty() {
return Err(ContractError::InvalidUpdate);
}
match t::merge(&space_id, &member, candidates.iter().map(|v| v.as_slice())) {
Some(bytes) => Ok(UpdateModification::valid(State::from(bytes))),
None => Ok(UpdateModification::valid(state)),
}
}
fn summarize_state(
parameters: Parameters<'static>,
state: State<'static>,
) -> Result<StateSummary<'static>, ContractError> {
let (space_id, member) = parts(&parameters)?;
if state.as_ref().is_empty() {
return Ok(StateSummary::from(Vec::new()));
}
let s = t::validate(&space_id, &member, state.as_ref())
.map_err(|e| ContractError::Other(format!("invalid holdings: {e}")))?;
Ok(StateSummary::from(summary_of(state.as_ref(), &s)))
}
fn get_state_delta(
parameters: Parameters<'static>,
state: State<'static>,
summary: StateSummary<'static>,
) -> Result<StateDelta<'static>, ContractError> {
let (space_id, member) = parts(&parameters)?;
if state.as_ref().is_empty() {
return Ok(StateDelta::from(Vec::new()));
}
let s = t::validate(&space_id, &member, state.as_ref())
.map_err(|e| ContractError::Other(format!("invalid holdings: {e}")))?;
let ours = t::rank(state.as_ref(), &s);
let theirs = {
let b = summary.as_ref();
if b.len() == 40 {
let seq = u64::from_le_bytes(b[..8].try_into().unwrap());
let mut h = [0u8; 32];
h.copy_from_slice(&b[8..40]);
Some((seq, h))
} else {
None
}
};
let newer = match theirs {
Some(t) => ours > t,
None => true,
};
Ok(StateDelta::from(if newer { state.as_ref().to_vec() } else { Vec::new() }))
}
}