forked from Keelrleek/SoulStream
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:
1 parent
08ca09586a
commit
a98c750908
12 files changed
+2955
-8
No files matched your search
@@ -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(¶meters)?;
|
||||
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(¶meters)?;
|
||||
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(¶meters)?;
|
||||
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(¶meters)?;
|
||||
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() }))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user