summaryrefslogtreecommitdiffstats
path: root/src/remake/records.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-24 13:09:22 +0200
committertv <tv@krebsco.de>2026-10-24 13:09:22 +0200
commit1df85d13453b717dbee8f4ebf47b87de4f647866 (patch)
treee86df90da61f74320c99aa5b19f5871c6918f09d /src/remake/records.rs
parentc852ea8478963f5298e5b186ca5e1d7284504fd1 (diff)
introduce majority voting on remakes
Diffstat (limited to 'src/remake/records.rs')
-rw-r--r--src/remake/records.rs38
1 files changed, 35 insertions, 3 deletions
diff --git a/src/remake/records.rs b/src/remake/records.rs
index 5c844f1..ed018ca 100644
--- a/src/remake/records.rs
+++ b/src/remake/records.rs
@@ -7,6 +7,7 @@
//! * `decision`: written once, by the coordinator only (committed or aborted).
//! * `abortRequested`: written by any node on an operator's request. It is an input to
//! the coordinator's decision, not a decision itself.
+//! * `votes/<pk>`: each written by that member only (`vote`).
//!
//! A proposal whose coordinator never decides (crash, partition) is reported as
//! `expired` once its settle period plus a grace margin has passed. Expiry is derived,
@@ -15,6 +16,8 @@
//! Records edited into an unreadable shape through the store API are skipped with a
//! warning.
+use std::collections::BTreeMap;
+
use automerge::{AutoCommit, ChangeHash};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
@@ -127,8 +130,10 @@ pub(crate) struct Proposal {
/// interval if absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) switch_delay_ms: Option<u64>,
- /// Peers that must stay active during the settle period.
- pub(crate) peers_considered: Vec<String>,
+ /// Members whose votes count (sorted), fixed at proposal time (`vote`).
+ pub(crate) electorate: Vec<String>,
+ /// Votes needed to commit: a majority of the electorate.
+ pub(crate) quorum: usize,
/// The remake kind (`compact` for records written before kinds existed).
#[serde(default = "default_kind")]
pub(crate) kind: String,
@@ -173,6 +178,13 @@ pub(crate) struct AbortRequest {
pub(crate) at: u64,
}
+/// A member's vote for a remake.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct Vote {
+ pub(crate) at: u64,
+}
+
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Record {
@@ -181,6 +193,9 @@ pub(crate) struct Record {
pub(crate) decision: Option<Decision>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) abort_requested: Option<AbortRequest>,
+ /// By member public key.
+ #[serde(default)]
+ pub(crate) votes: BTreeMap<String, Vote>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
@@ -316,6 +331,18 @@ pub(crate) fn write_abort_request(
write_part(doc, id, "abortRequested", request)
}
+pub(crate) fn write_vote(
+ doc: &mut AutoCommit,
+ id: &str,
+ voter: &str,
+ vote: &Vote,
+) -> Result<(), StoreError> {
+ let json = serde_json::to_value(vote).map_err(anyhow::Error::from)?;
+ let mut path = record_path(id, "votes");
+ path.push(voter.to_owned());
+ store::write_path(doc, &path, &json)
+}
+
pub(crate) fn decode_hex32(hex: &str) -> Option<[u8; 32]> {
if hex.len() != 64 || !hex.is_ascii() {
return None;
@@ -360,7 +387,8 @@ mod tests {
proposed_at: settle_until.saturating_sub(1_000),
settle_until,
switch_delay_ms: None,
- peers_considered: vec!["node-b".into()],
+ electorate: vec!["node-a".into(), "node-b".into()],
+ quorum: 2,
kind: DEFAULT_KIND.to_owned(),
content: JsonValue::Null,
translation: JsonValue::Null,
@@ -390,6 +418,7 @@ mod tests {
decided_at: 11_000,
};
write_decision(&mut doc, "c-1", &decision).unwrap();
+ write_vote(&mut doc, "c-1", "node-b", &Vote { at: 7 }).unwrap();
doc.commit();
let all = load_all(&doc);
@@ -399,6 +428,8 @@ mod tests {
assert_eq!(record.proposal.policy, p.policy);
assert_eq!(record.proposal.base_change_hashes().unwrap().len(), 1);
assert!(record.abort_requested.is_some());
+ assert_eq!(record.votes.keys().collect::<Vec<_>>(), ["node-b"]);
+ assert_eq!(record.proposal.quorum, 2);
assert_eq!(record.phase(0, 0), Phase::Committed);
}
@@ -408,6 +439,7 @@ mod tests {
proposal: proposal(10_000),
decision: None,
abort_requested: None,
+ votes: BTreeMap::new(),
};
assert_eq!(record.phase(10_000, 500), Phase::Proposed);
assert_eq!(record.phase(10_500, 500), Phase::Proposed);