Skip to content

Commit bbdd4b6

Browse files
authored
Trust Quorum: Better match protocol in TLA+ spec (#8363)
This PR changes the message implementation a bit and the `PersistentState` structure. It adds messages that will be used in follow up PRs, and ensures that `rack_id` is always checked on each message. Furthemore it stores configurations and shares separately inside the persistent state. This allows retrieval of a configuration for stale sled and the recomputation of its own share for that configuration to occur in separate steps. This matches what is done in the TLA+ spec. I also added a comment about the intention of future code coming for the `Node` type. This reflects the experience of implementing LRTQ, and is helpful to simplify testing. If we don't have to track timeouts for arbitrary API requests inside the sans-io code, it becomes much simpler.
1 parent 45cea5d commit bbdd4b6

File tree

10 files changed

+225
-147
lines changed

10 files changed

+225
-147
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

trust-quorum/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ derive_more.workspace = true
1616
gfss.workspace = true
1717
hex.workspace = true
1818
hkdf.workspace = true
19+
iddqd.workspace = true
1920
rand = { workspace = true, features = ["getrandom"] }
2021
secrecy.workspace = true
2122
serde.workspace = true

trust-quorum/src/configuration.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::crypto::{EncryptedRackSecret, RackSecret, Salt, Sha3_256Digest};
88
use crate::validators::ValidatedReconfigureMsg;
99
use crate::{Epoch, PlatformId, Threshold};
1010
use gfss::shamir::{Share, SplitError};
11+
use iddqd::{IdOrdItem, id_upcast};
1112
use omicron_uuid_kinds::RackUuid;
1213
use secrecy::ExposeSecret;
1314
use serde::{Deserialize, Serialize};
@@ -50,6 +51,16 @@ pub struct Configuration {
5051
pub previous_configuration: Option<PreviousConfiguration>,
5152
}
5253

54+
impl IdOrdItem for Configuration {
55+
type Key<'a> = Epoch;
56+
57+
fn key(&self) -> Self::Key<'_> {
58+
self.epoch
59+
}
60+
61+
id_upcast!();
62+
}
63+
5364
impl Configuration {
5465
/// Create a new configuration for the trust quorum
5566
///

trust-quorum/src/coordinator_state.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
//! State of a reconfiguration coordinator inside a [`crate::Node`]
66
77
use crate::crypto::{LrtqShare, Sha3_256Digest, ShareDigestLrtq};
8-
use crate::messages::{PeerMsg, PrepareMsg};
8+
use crate::messages::PeerMsg;
99
use crate::validators::{ReconfigurationError, ValidatedReconfigureMsg};
10-
use crate::{Configuration, Envelope, Epoch, PlatformId};
10+
use crate::{Configuration, Envelope, Epoch, PeerMsgKind, PlatformId};
1111
use gfss::shamir::Share;
1212
use slog::{Logger, o, warn};
1313
use std::collections::{BTreeMap, BTreeSet};
@@ -55,37 +55,37 @@ impl CoordinatorState {
5555
log: Logger,
5656
now: Instant,
5757
msg: ValidatedReconfigureMsg,
58-
) -> Result<(CoordinatorState, PrepareMsg), ReconfigurationError> {
58+
) -> Result<(CoordinatorState, Configuration, Share), ReconfigurationError>
59+
{
5960
// Create a configuration for this epoch
6061
let (config, shares) = Configuration::new(&msg)?;
6162

6263
let mut prepares = BTreeMap::new();
63-
// `my_prepare_msg` is optional only so that we can fill it in via
64-
// the loop. It will always become `Some`, as a `Configuration` always
64+
// `my_share` is optional only so that we can fill it in via the
65+
// loop. It will always become `Some`, as a `Configuration` always
6566
// contains the coordinator as a member as validated by construction of
6667
// `ValidatedReconfigureMsg`.
67-
let mut my_prepare_msg: Option<PrepareMsg> = None;
68+
let mut my_share: Option<Share> = None;
6869
for (platform_id, share) in shares.into_iter() {
69-
let prepare_msg = PrepareMsg { config: config.clone(), share };
7070
if platform_id == *msg.coordinator_id() {
71-
// The prepare message to add to our `PersistentState`
72-
my_prepare_msg = Some(prepare_msg);
71+
// The data to add to our `PersistentState`
72+
my_share = Some(share);
7373
} else {
7474
// Create a message that requires sending
75-
prepares.insert(platform_id, prepare_msg);
75+
prepares.insert(platform_id, (config.clone(), share));
7676
}
7777
}
7878
let op = CoordinatorOperation::Prepare {
7979
prepares,
8080
prepare_acks: BTreeSet::new(),
8181
};
8282

83-
let state = CoordinatorState::new(log, now, msg, config, op);
83+
let state = CoordinatorState::new(log, now, msg, config.clone(), op);
8484

8585
// Safety: Construction of a `ValidatedReconfigureMsg` ensures that
8686
// `my_platform_id` is part of the new configuration and has a share.
8787
// We can therefore safely unwrap here.
88-
Ok((state, my_prepare_msg.unwrap()))
88+
Ok((state, config, my_share.unwrap()))
8989
}
9090

9191
/// A reconfiguration from one group to another
@@ -161,11 +161,17 @@ impl CoordinatorState {
161161
} => {}
162162
CoordinatorOperation::CollectLrtqShares { members, shares } => {}
163163
CoordinatorOperation::Prepare { prepares, prepare_acks } => {
164-
for (platform_id, prepare) in prepares.clone().into_iter() {
164+
let rack_id = self.reconfigure_msg.rack_id();
165+
for (platform_id, (config, share)) in
166+
prepares.clone().into_iter()
167+
{
165168
outbox.push(Envelope {
166169
to: platform_id,
167170
from: self.reconfigure_msg.coordinator_id().clone(),
168-
msg: PeerMsg::Prepare(prepare),
171+
msg: PeerMsg {
172+
rack_id,
173+
kind: PeerMsgKind::Prepare { config, share },
174+
},
169175
});
170176
}
171177
}
@@ -226,7 +232,7 @@ pub enum CoordinatorOperation {
226232
},
227233
Prepare {
228234
/// The set of Prepares to send to each node
229-
prepares: BTreeMap<PlatformId, PrepareMsg>,
235+
prepares: BTreeMap<PlatformId, (Configuration, Share)>,
230236

231237
/// Acknowledgements that the prepare has been received
232238
prepare_acks: BTreeSet<PlatformId>,

trust-quorum/src/messages.rs

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,39 +27,70 @@ pub struct ReconfigureMsg {
2727

2828
/// Messages sent between trust quorum members over a sprockets channel
2929
#[derive(Debug, Clone, Serialize, Deserialize)]
30-
pub enum PeerMsg {
31-
Prepare(PrepareMsg),
30+
pub struct PeerMsg {
31+
pub rack_id: RackUuid,
32+
pub kind: PeerMsgKind,
33+
}
34+
35+
#[derive(Debug, Clone, Serialize, Deserialize)]
36+
pub enum PeerMsgKind {
37+
/// Sent from a coordinator node to inform a peer about a new configuration
38+
Prepare {
39+
config: Configuration,
40+
share: Share,
41+
},
42+
43+
/// Acknowledge a successful prepare from a coordinator
3244
PrepareAck(Epoch),
33-
Commit(CommitMsg),
3445

35-
/// When a node learns about a commit for a given epoch
36-
/// but does not have a `PrepareMsg`, it must ask for it
37-
/// from another node.
38-
GetPrepare(Epoch),
46+
/// Retrieve a configuration for a given epoch from a node. Nodes only
47+
/// respond if this is the current configuration and the requesting node is
48+
/// a member of the configuration.
49+
GetConfig(Epoch),
3950

40-
/// Nodes reply with `PrepareAndCommit` to `GetPrepare` requests when they
41-
/// are able to.
42-
PrepareAndCommit,
51+
/// A configuration returned in response to `GetConfig`
52+
Config(Configuration),
4353

54+
/// Request a node's key share for the given epoch from that node
4455
GetShare(Epoch),
56+
57+
/// Return a node's key share in response to a `GetShare` message
4558
Share {
4659
epoch: Epoch,
4760
share: Share,
4861
},
4962

5063
// LRTQ shares are always at epoch 0
5164
GetLrtqShare,
65+
5266
LrtqShare(LrtqShare),
53-
}
5467

55-
/// The start of a reconfiguration sent from a coordinator to a specific peer
56-
#[derive(Debug, Clone, Serialize, Deserialize)]
57-
pub struct PrepareMsg {
58-
pub config: Configuration,
59-
pub share: Share,
68+
/// Inform a node that it is no longer part of the trust quorum as of the
69+
/// given epoch
70+
Expunged(Epoch),
71+
72+
/// Inform a node that it is utilizing an old committed onfiguration and
73+
/// give it the new configuration.
74+
///
75+
/// As a result, a requesting node may have to retrieve key shares to
76+
/// recompute its share if it never received a prepare message for this
77+
/// epoch.
78+
CommitAdvance(Configuration),
6079
}
6180

62-
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63-
pub struct CommitMsg {
64-
epoch: Epoch,
81+
impl PeerMsgKind {
82+
pub fn name(&self) -> &str {
83+
match self {
84+
Self::Prepare { .. } => "prepare",
85+
Self::PrepareAck(_) => "prepare_ack",
86+
Self::GetConfig(_) => "get_config",
87+
Self::Config(_) => "config",
88+
Self::GetShare(_) => "get_share",
89+
Self::Share { .. } => "share",
90+
Self::GetLrtqShare => "get_lrtq_share",
91+
Self::LrtqShare(_) => "lrtq_share",
92+
Self::Expunged(_) => "expunged",
93+
Self::CommitAdvance(_) => "commit_advance",
94+
}
95+
}
6596
}

trust-quorum/src/node.rs

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,23 @@
33
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
44

55
//! A trust quorum node that implements the trust quorum protocol
6+
//!
7+
//! Nodes respond to function calls synchronously. They do not queue up replies
8+
//! for later. If a request like a `prepare_and_commit` operation arrives from
9+
//! Nexus, a node may not be ready to commit immediately. It may have to reach
10+
//! out to other nodes to get its configuration and collect shares. None of
11+
//! this happens in one tick through this FSM. Instead, outgoing messages will
12+
//! be queued for sending and the call will return. Higher level software must
13+
//! keep track of incoming requests and poll this FSM for responses. This makes
14+
//! the logical code here simpler at the cost of implementing tracking at higher
15+
//! levels. Fortunately, tracking is easier with async code, which drives this
16+
//! Node, and so this should not be problematic.
617
718
use crate::validators::{ReconfigurationError, ValidatedReconfigureMsg};
819
use crate::{
920
CoordinatorState, Envelope, Epoch, PersistentState, PlatformId, messages::*,
1021
};
11-
12-
use slog::{Logger, o, warn};
22+
use slog::{Logger, error, o, warn};
1323
use std::time::Instant;
1424

1525
/// An entity capable of participating in trust quorum
@@ -92,8 +102,18 @@ impl Node {
92102
from: PlatformId,
93103
msg: PeerMsg,
94104
) -> Option<PersistentState> {
95-
match msg {
96-
PeerMsg::PrepareAck(epoch) => {
105+
if let Some(rack_id) = self.persistent_state.rack_id() {
106+
if rack_id != msg.rack_id {
107+
error!(self.log, "Mismatched rack id";
108+
"from" => %from,
109+
"msg" => msg.kind.name(),
110+
"expected" => %rack_id,
111+
"got" => %msg.rack_id);
112+
return None;
113+
}
114+
}
115+
match msg.kind {
116+
PeerMsgKind::PrepareAck(epoch) => {
97117
self.handle_prepare_ack(from, epoch);
98118
None
99119
}
@@ -160,24 +180,25 @@ impl Node {
160180
) -> Result<Option<PersistentState>, ReconfigurationError> {
161181
// We have no committed configuration or lrtq ledger
162182
if self.persistent_state.is_uninitialized() {
163-
let (coordinator_state, my_prepare_msg) =
183+
let (coordinator_state, my_config, my_share) =
164184
CoordinatorState::new_uninitialized(
165185
self.log.clone(),
166186
now,
167187
msg,
168188
)?;
169189
self.coordinator_state = Some(coordinator_state);
170-
// Add the prepare to our `PersistentState`
190+
self.persistent_state.shares.insert(my_config.epoch, my_share);
171191
self.persistent_state
172-
.prepares
173-
.insert(my_prepare_msg.config.epoch, my_prepare_msg);
192+
.configs
193+
.insert_unique(my_config)
194+
.expect("empty state");
174195

175196
return Ok(Some(self.persistent_state.clone()));
176197
}
177198

178199
// We have a committed configuration that is not LRTQ
179200
let config =
180-
self.persistent_state.last_committed_configuration().unwrap();
201+
self.persistent_state.latest_committed_configuration().unwrap();
181202

182203
self.coordinator_state = Some(CoordinatorState::new_reconfiguration(
183204
self.log.clone(),
@@ -258,16 +279,13 @@ mod tests {
258279
// It should include the `PrepareMsg` for this node.
259280
assert!(persistent_state.lrtq.is_none());
260281
assert!(persistent_state.commits.is_empty());
261-
assert!(persistent_state.decommissioned.is_none());
262-
// The only `PrepareMsg` is this one for the first epoch
263-
assert_eq!(persistent_state.prepares.len(), 1);
282+
assert!(persistent_state.expunged.is_none());
283+
assert_eq!(persistent_state.configs.len(), 1);
284+
assert_eq!(persistent_state.shares.len(), 1);
264285

265286
// Extract the configuration for our initial prepare msg
266-
let config = &persistent_state
267-
.prepares
268-
.get(&input.reconfigure_msg.epoch)
269-
.unwrap()
270-
.config;
287+
let config =
288+
persistent_state.configs.get(&input.reconfigure_msg.epoch).unwrap();
271289

272290
assert_eq!(config.epoch, input.reconfigure_msg.epoch);
273291
assert_eq!(config.coordinator, *node.platform_id());
@@ -280,8 +298,8 @@ mod tests {
280298
assert_eq!(outbox.len(), config.members.len() - 1);
281299
for envelope in outbox {
282300
assert_matches!(
283-
envelope.msg,
284-
PeerMsg::Prepare(PrepareMsg { config: prepare_config, .. }) => {
301+
envelope.msg.kind,
302+
PeerMsgKind::Prepare{ config: prepare_config, .. } => {
285303
assert_eq!(*config, prepare_config);
286304
});
287305
assert_eq!(envelope.from, config.coordinator);

0 commit comments

Comments
 (0)