the proposal board: a Space's pinned playlist takes signed proposals, and an accept republishes it
This commit is contained in:
1 parent
8b511b20e7
commit
bf81792c49
15 files changed
+3207
-10
No files matched your search
Generated
+1449
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "soulstream-proposal-contract"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "The SoulStream proposal-board contract — a thin shell over persona_state::proposals::merge_board. Built to wasm32-unknown-unknown and bundled into the app; its code hash is a commitment (see src-tauri/assets)."
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
freenet-stdlib = { version = "=0.8.5" }
|
||||
persona-state = { path = "../persona-state" }
|
||||
bincode = "1"
|
||||
blake3 = "1"
|
||||
|
||||
[features]
|
||||
default = ["freenet-main-contract", "contract"]
|
||||
freenet-main-contract = []
|
||||
contract = ["freenet-stdlib/contract"]
|
||||
trace = ["freenet-stdlib/trace"]
|
||||
|
||||
[profile.release]
|
||||
strip = true
|
||||
lto = true
|
||||
|
||||
# Its own workspace: this crate targets wasm32 and must never join the
|
||||
# app's host build.
|
||||
[workspace]
|
||||
@@ -0,0 +1,118 @@
|
||||
//! The SoulStream proposal-board contract. Multi-writer: the owner offers
|
||||
//! checkpoints and decisions, contributors offer proposals; `persona_state::proposals::merge_board`
|
||||
//! is the bouncer (owner signature, the current roster, decided-tombstones,
|
||||
//! the caps). Deterministic by construction, so every delivery order
|
||||
//! converges; a state that does not merge to itself is refused.
|
||||
//!
|
||||
//! Build: `cargo build --release --target wasm32-unknown-unknown` in this
|
||||
//! directory; copy `target/wasm32-unknown-unknown/release/
|
||||
//! soulstream_proposal_contract.wasm` to `src-tauri/assets/proposal_contract.wasm`
|
||||
//! and re-pin the code hash in `src-tauri/src/proposals.rs`.
|
||||
|
||||
use freenet_stdlib::prelude::*;
|
||||
use persona_state::proposals as t;
|
||||
|
||||
pub struct Contract;
|
||||
|
||||
fn parts(parameters: &Parameters<'static>) -> Result<([u8; 32], [u8; 16]), ContractError> {
|
||||
t::params_parts(parameters.as_ref())
|
||||
.ok_or_else(|| ContractError::Other("params are not family+owner+uuid".into()))
|
||||
}
|
||||
|
||||
fn collect<'a>(state: &'a State<'static>, data: &'a [UpdateData<'static>]) -> (Vec<&'a [u8]>, bool) {
|
||||
let mut candidates: Vec<&[u8]> = Vec::new();
|
||||
if !state.as_ref().is_empty() {
|
||||
candidates.push(state.as_ref());
|
||||
}
|
||||
let mut any_parse = false;
|
||||
let mut push = |b: &'a [u8]| {
|
||||
if !b.is_empty() {
|
||||
any_parse |= bincode::deserialize::<t::BoardState>(b).is_ok();
|
||||
candidates.push(b);
|
||||
}
|
||||
};
|
||||
for ud in data {
|
||||
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());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(candidates, any_parse)
|
||||
}
|
||||
|
||||
#[contract]
|
||||
impl ContractInterface for Contract {
|
||||
fn validate_state(
|
||||
parameters: Parameters<'static>,
|
||||
state: State<'static>,
|
||||
_related: RelatedContracts<'static>,
|
||||
) -> Result<ValidateResult, ContractError> {
|
||||
let (owner, uuid) = parts(¶meters)?;
|
||||
let bytes = state.as_ref();
|
||||
if bytes.is_empty() {
|
||||
return Ok(ValidateResult::Valid);
|
||||
}
|
||||
if bytes.len() > t::MAX_STATE_BYTES {
|
||||
return Err(ContractError::Other(format!(
|
||||
"board state {} bytes over cap {}",
|
||||
bytes.len(),
|
||||
t::MAX_STATE_BYTES
|
||||
)));
|
||||
}
|
||||
let merged = t::merge_board(&owner, &uuid, &[bytes]);
|
||||
let canon = bincode::serialize(&merged).map_err(|e| ContractError::Deser(e.to_string()))?;
|
||||
if canon != bytes {
|
||||
return Err(ContractError::Other(
|
||||
"state is not canonical (unsigned, off-roster, decided, or over a cap)".into(),
|
||||
));
|
||||
}
|
||||
Ok(ValidateResult::Valid)
|
||||
}
|
||||
|
||||
fn update_state(
|
||||
parameters: Parameters<'static>,
|
||||
state: State<'static>,
|
||||
data: Vec<UpdateData<'static>>,
|
||||
) -> Result<UpdateModification<'static>, ContractError> {
|
||||
let (owner, uuid) = parts(¶meters)?;
|
||||
let (candidates, any_parse) = collect(&state, &data);
|
||||
if !any_parse && !data.is_empty() {
|
||||
return Err(ContractError::InvalidUpdate);
|
||||
}
|
||||
let merged = t::merge_board(&owner, &uuid, &candidates);
|
||||
let bytes = bincode::serialize(&merged).map_err(|e| ContractError::Deser(e.to_string()))?;
|
||||
Ok(UpdateModification::valid(State::from(bytes)))
|
||||
}
|
||||
|
||||
fn summarize_state(
|
||||
parameters: Parameters<'static>,
|
||||
state: State<'static>,
|
||||
) -> Result<StateSummary<'static>, ContractError> {
|
||||
let _ = parts(¶meters)?;
|
||||
if state.as_ref().is_empty() {
|
||||
return Ok(StateSummary::from(Vec::new()));
|
||||
}
|
||||
Ok(StateSummary::from(blake3::hash(state.as_ref()).as_bytes().to_vec()))
|
||||
}
|
||||
|
||||
fn get_state_delta(
|
||||
parameters: Parameters<'static>,
|
||||
state: State<'static>,
|
||||
summary: StateSummary<'static>,
|
||||
) -> Result<StateDelta<'static>, ContractError> {
|
||||
let _ = parts(¶meters)?;
|
||||
if state.as_ref().is_empty() {
|
||||
return Ok(StateDelta::from(Vec::new()));
|
||||
}
|
||||
if summary.as_ref() == blake3::hash(state.as_ref()).as_bytes() {
|
||||
Ok(StateDelta::from(Vec::new()))
|
||||
} else {
|
||||
Ok(StateDelta::from(state.as_ref().to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user