17.6 KiB, 3 runs
created by r2848102244:393, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The content key, and what a carrier is given instead of the content. |
| 2 | //! |
| 3 | //! A repository hosted on a relay is private by the relay's access list, which |
| 4 | //! is a promise about how the relay behaves. A *veiled* repository is private |
| 5 | //! whatever the relay does: every operation crosses as a [`Veiled`] entry, whose |
| 6 | //! identifier and parents are in clear and whose body is encrypted under a key |
| 7 | //! the relay never holds. The relay can then do the whole of its job -- walk |
| 8 | //! frontiers, reconcile sketches, carry operations, converge two replicas that |
| 9 | //! are never awake together -- while being unable to read a byte of what they |
| 10 | //! say. |
| 11 | //! |
| 12 | //! # The key belongs to the readers |
| 13 | //! |
| 14 | //! One symmetric key per repository, AES-256-GCM, held in `.ore/veil` beside the |
| 15 | //! signing key and written under the same file mode. It is never deposited with |
| 16 | //! a relay, never carried in a request and never named in a segment. A second |
| 17 | //! replica joins by being given the key through a channel the relay is not part |
| 18 | //! of, which is `ore key --veil <key>`, and that hand-over is the whole of the |
| 19 | //! trust decision. |
| 20 | //! |
| 21 | //! Wrapping the key to each collaborator's public key, so it could travel |
| 22 | //! through the relay itself, is the obvious next thing and is deliberately not |
| 23 | //! here. It waits on a decision about identity rather than on a format, and the |
| 24 | //! decision was taken on 2026-08-17: a wrap is received by a second key of a |
| 25 | //! second kind, published by a binding of its own. |
| 26 | //! |
| 27 | //! An earlier version of this comment gave the reason as impossibility -- that an |
| 28 | //! Ore replica key is an Ed25519 *signing* key and so cannot receive a wrapped |
| 29 | //! secret. **That was wrong.** Ed25519 and X25519 are the same curve in two |
| 30 | //! coordinate systems and `ed25519-dalek` ships the conversion, so the identity |
| 31 | //! could have received a wrap directly. It was refused because the library's own |
| 32 | //! documentation argues against reusing a signing key for key agreement, not |
| 33 | //! because it could not be done. See `doc/design/key_distribution.md`. |
| 34 | //! |
| 35 | //! # Veiling is for the wire, not for the disk |
| 36 | //! |
| 37 | //! A working copy's own segments hold plain entries. Veiling them would buy |
| 38 | //! nothing, since `.ore/veil` sits in the same directory as the plaintext |
| 39 | //! `.ore/key`, and anyone who can read one can read the other. So a veil goes on |
| 40 | //! at the moment an entry is handed to a carrier and comes off the moment one |
| 41 | //! arrives, and every verb but `sync` is untouched by it. |
| 42 | //! |
| 43 | //! # What a carrier holds instead |
| 44 | //! |
| 45 | //! A relay has to put arriving operations into an [`OpLog`], which holds whole |
| 46 | //! records, and a veiled entry has no record to give it. So the relay holds a |
| 47 | //! [`standin`] in the log -- the true header, and a mark saying in words that |
| 48 | //! the operation is veiled -- and keeps the veiled entry itself beside it, by |
| 49 | //! identifier, exactly as it keeps envelopes. Everything the relay does with the |
| 50 | //! log reads the header and never the operation, and everything the relay hands |
| 51 | //! back or writes down is substituted from the veiled entries it kept. A |
| 52 | //! stand-in is refused by [`crate::store::Store::append`] if it ever reaches the |
| 53 | //! disk, because a stand-in written down would be a veiled operation lost. |
| 54 | //! |
| 55 | //! # The honest limit |
| 56 | //! |
| 57 | //! Veiling hides operation content and the size of it. The shape of the graph, |
| 58 | //! the number of operations, when they arrived, which replicas wrote them and |
| 59 | //! which public keys signed them are all still visible to the carrier, because |
| 60 | //! they are what the carrier is for. |
| 61 | |
| 62 | use oxedyne_fe2o3_core::prelude::*; |
| 63 | use oxedyne_fe2o3_crypto::enc::EncryptionScheme; |
| 64 | use oxedyne_fe2o3_iop_crypto::keys::KeyManager; |
| 65 | use oxedyne_fe2o3_jdat::prelude::*; |
| 66 | use oxedyne_fe2o3_ore::id::OpId; |
| 67 | use oxedyne_fe2o3_ore::op::{ |
| 68 | Header, |
| 69 | Op, |
| 70 | Record, |
| 71 | }; |
| 72 | use oxedyne_fe2o3_ore::segment::{ |
| 73 | Entry, |
| 74 | Veiled, |
| 75 | }; |
| 76 | use oxedyne_fe2o3_ore::sync::Message; |
| 77 | |
| 78 | use crate::keys::{ |
| 79 | bytes_of, |
| 80 | text_of, |
| 81 | write_private, |
| 82 | }; |
| 83 | |
| 84 | use std::collections::BTreeMap; |
| 85 | use std::fs; |
| 86 | use std::path::{ |
| 87 | Path, |
| 88 | PathBuf, |
| 89 | }; |
| 90 | |
| 91 | |
| 92 | /// Name of the content key file, within the `.ore` directory. |
| 93 | pub const VEIL_FILE: &str = "veil"; |
| 94 | |
| 95 | /// Version of the content key file this tool writes. |
| 96 | pub const VEIL_VERSION: u64 = 1; |
| 97 | |
| 98 | /// The name the content key file records the cipher under. |
| 99 | pub const CIPHER: &str = "AES-256-GCM"; |
| 100 | |
| 101 | /// Length of a content key, in bytes. |
| 102 | pub const KEY_LEN: usize = 32; |
| 103 | |
| 104 | /// What the content key file says about itself. |
| 105 | pub const VEIL_COMMENT: &str = "This content key is NOT encrypted. Anyone who can read this \ |
| 106 | file can read every operation this repository sends to a relay, so the file is written \ |
| 107 | readable only by its owner (mode 0600). It is never sent to a relay: a replica is given it \ |
| 108 | by hand."; |
| 109 | |
| 110 | |
| 111 | /// The name a stand-in mark carries, so that one is recognisable wherever it |
| 112 | /// turns up. |
| 113 | /// |
| 114 | /// It says what it is in words rather than in a sentinel byte, because the place |
| 115 | /// it might turn up is somebody's screen. |
| 116 | pub fn standin_name(id: OpId) -> String { |
| 117 | fmt!("veiled operation {}", id) |
| 118 | } |
| 119 | |
| 120 | /// The record a carrier holds in place of one it cannot read. |
| 121 | /// |
| 122 | /// The header is the true one, which is what every question a carrier asks of a |
| 123 | /// log is answered from. The operation is a mark, which renders nothing, claims |
| 124 | /// no byte and mints no atom, so a stand-in that somehow reached a renderer would |
| 125 | /// add nothing to what it drew; and it is named after the operation it stands |
| 126 | /// for, so a stand-in that somehow reached a person would say so. |
| 127 | pub fn standin(head: Header) -> Record { |
| 128 | let name = standin_name(head.id()); |
| 129 | Record::new(head, Op::Mark { name, body: None, time: None }) |
| 130 | } |
| 131 | |
| 132 | /// Is this record the stand-in a carrier holds for a veiled operation? |
| 133 | pub fn is_standin(rec: &Record) -> bool { |
| 134 | match &rec.op { |
| 135 | Op::Mark { name, body: None, time: None } => *name == standin_name(rec.id()), |
| 136 | _ => false, |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | |
| 141 | /// One repository's content key. |
| 142 | #[derive(Clone, Debug)] |
| 143 | pub struct Veil { |
| 144 | /// The cipher, holding the key. |
| 145 | scheme: EncryptionScheme, |
| 146 | /// The key itself, kept beside the cipher because installing it elsewhere |
| 147 | /// asks for it. |
| 148 | key: Vec<u8>, |
| 149 | } |
| 150 | |
| 151 | impl Veil { |
| 152 | |
| 153 | /// Returns the path of the content key file within the `.ore` directory |
| 154 | /// `dir`. |
| 155 | pub fn path_of(dir: &Path) -> PathBuf { |
| 156 | dir.join(VEIL_FILE) |
| 157 | } |
| 158 | |
| 159 | /// Mints a fresh content key. |
| 160 | pub fn mint() |
| 161 | -> Outcome<Self> |
| 162 | { |
| 163 | let scheme = EncryptionScheme::new_aes_256_gcm(); |
| 164 | let key = match res!(scheme.get_secret_key()) { |
| 165 | Some(sk) => sk.to_vec(), |
| 166 | None => return Err(err!( |
| 167 | "A freshly minted {} key holds no key, which cannot happen and means \ |
| 168 | the scheme has changed under this tool.", CIPHER; |
| 169 | Bug, Missing, Key)), |
| 170 | }; |
| 171 | Ok(Self { scheme, key }) |
| 172 | } |
| 173 | |
| 174 | /// Takes a key somebody was handed, refusing one the cipher will not accept. |
| 175 | pub fn of(key: &[u8]) |
| 176 | -> Outcome<Self> |
| 177 | { |
| 178 | if key.len() != KEY_LEN { |
| 179 | return Err(err!( |
| 180 | "A content key is {} bytes and this one is {}. It is the text `ore key \ |
| 181 | --veil` printed in the repository it came from, whole.", |
| 182 | KEY_LEN, key.len(); |
| 183 | Invalid, Input, Key)); |
| 184 | } |
| 185 | Ok(Self { |
| 186 | scheme: res!(EncryptionScheme::new_aes_256_gcm_with_key(key)), |
| 187 | key: key.to_vec(), |
| 188 | }) |
| 189 | } |
| 190 | |
| 191 | /// Returns the key in the form the file holds it and `ore key --veil` prints |
| 192 | /// it. |
| 193 | pub fn text(&self) -> String { |
| 194 | text_of(&self.key) |
| 195 | } |
| 196 | |
| 197 | /// Reads the content key file, which a repository is not obliged to have. |
| 198 | /// |
| 199 | /// Absence is `None`, because a repository that veils nothing is the ordinary |
| 200 | /// case; a file that is there and will not read is an error, because the |
| 201 | /// alternative is quietly handing a relay the plaintext of a repository whose |
| 202 | /// owner asked for the opposite. |
| 203 | pub fn read(dir: &Path) |
| 204 | -> Outcome<Option<Self>> |
| 205 | { |
| 206 | let path = Self::path_of(dir); |
| 207 | if !path.is_file() { |
| 208 | return Ok(None); |
| 209 | } |
| 210 | let text = match fs::read_to_string(&path) { |
| 211 | Ok(t) => t, |
| 212 | Err(e) => return Err(err!(e, |
| 213 | "The content key {:?} could not be read.", path; |
| 214 | IO, File, Read)), |
| 215 | }; |
| 216 | let dat = match Dat::decode_string(text) { |
| 217 | Ok(d) => d, |
| 218 | Err(e) => return Err(err!(e, |
| 219 | "The content key {:?} is not readable JDAT.", path; |
| 220 | Decode, Input)), |
| 221 | }; |
| 222 | let map = match &dat { |
| 223 | Dat::Map(m) => m, |
| 224 | other => return Err(err!( |
| 225 | "The content key {:?} expects a map, got {:?}.", path, other; |
| 226 | Decode, Input, Mismatch)), |
| 227 | }; |
| 228 | let field = |key: &str| -> Outcome<Dat> { |
| 229 | match map.get(&Dat::Str(fmt!("{}", key))) { |
| 230 | Some(d) => Ok(d.clone()), |
| 231 | None => Err(err!( |
| 232 | "The content key {:?} has no field {:?}.", path, key; |
| 233 | Decode, Input, Missing)), |
| 234 | } |
| 235 | }; |
| 236 | let version = match res!(field("format")) { |
| 237 | Dat::U64(n) => n, |
| 238 | Dat::U32(n) => n as u64, |
| 239 | Dat::U8(n) => n as u64, |
| 240 | other => return Err(err!( |
| 241 | "The content key {:?} declares a format of {:?} rather than a number.", |
| 242 | path, other; |
| 243 | Decode, Input, Mismatch)), |
| 244 | }; |
| 245 | if version != VEIL_VERSION { |
| 246 | return Err(err!( |
| 247 | "The content key {:?} declares format version {}, and this tool knows \ |
| 248 | only version {}.", path, version, VEIL_VERSION; |
| 249 | Decode, Input, Version, Mismatch)); |
| 250 | } |
| 251 | let named = match res!(field("cipher")) { |
| 252 | Dat::Str(s) => s, |
| 253 | other => return Err(err!( |
| 254 | "The content key {:?} names its cipher {:?} rather than a string.", |
| 255 | path, other; |
| 256 | Decode, Input, Mismatch)), |
| 257 | }; |
| 258 | if named != CIPHER { |
| 259 | return Err(err!( |
| 260 | "The content key {:?} is a {} key, and this tool veils with {}.", |
| 261 | path, named, CIPHER; |
| 262 | Invalid, Input, Mismatch)); |
| 263 | } |
| 264 | let key = match res!(field("key")) { |
| 265 | Dat::Str(s) => res!(bytes_of(&s)), |
| 266 | other => return Err(err!( |
| 267 | "The content key {:?} holds {:?} rather than a string.", path, other; |
| 268 | Decode, Input, Mismatch)), |
| 269 | }; |
| 270 | Ok(Some(res!(Self::of(&key)))) |
| 271 | } |
| 272 | |
| 273 | /// Writes the content key file, readable only by its owner. |
| 274 | pub fn write(&self, dir: &Path) |
| 275 | -> Outcome<PathBuf> |
| 276 | { |
| 277 | let mut map = DaticleMap::new(); |
| 278 | map.insert(Dat::Str(fmt!("comment")), Dat::Str(fmt!("{}", VEIL_COMMENT))); |
| 279 | map.insert(Dat::Str(fmt!("format")), Dat::U64(VEIL_VERSION)); |
| 280 | map.insert(Dat::Str(fmt!("cipher")), Dat::Str(fmt!("{}", CIPHER))); |
| 281 | map.insert(Dat::Str(fmt!("key")), Dat::Str(self.text())); |
| 282 | let text = res!(Dat::Map(map).jdat_to_lines(" ")); |
| 283 | let path = Self::path_of(dir); |
| 284 | res!(write_private(&path, fmt!("{}\n", text).as_bytes())); |
| 285 | Ok(path) |
| 286 | } |
| 287 | |
| 288 | /// Puts a veil around every entry a message carries. |
| 289 | pub fn veil(&self, msg: Message) |
| 290 | -> Outcome<Message> |
| 291 | { |
| 292 | let entries = match msg { |
| 293 | Message::Send { entries } => entries, |
| 294 | other => return Ok(other), |
| 295 | }; |
| 296 | let mut out = Vec::with_capacity(entries.len()); |
| 297 | for entry in entries { |
| 298 | out.push(res!(entry.veil(&self.scheme))); |
| 299 | } |
| 300 | Ok(Message::Send { entries: out }) |
| 301 | } |
| 302 | |
| 303 | /// Takes the veil off every entry a message carries, and leaves a plain one |
| 304 | /// alone. |
| 305 | /// |
| 306 | /// A message that mixes the two is not a fault: a repository is veiled from |
| 307 | /// the moment somebody veils it, and the operations written before that |
| 308 | /// crossed in the clear and are still on the relay in the clear. |
| 309 | pub fn unveil(&self, msg: Message) |
| 310 | -> Outcome<Message> |
| 311 | { |
| 312 | let entries = match msg { |
| 313 | Message::Send { entries } => entries, |
| 314 | other => return Ok(other), |
| 315 | }; |
| 316 | let mut out = Vec::with_capacity(entries.len()); |
| 317 | for entry in entries { |
| 318 | out.push(if entry.is_veiled() { |
| 319 | res!(entry.unveil(&self.scheme)) |
| 320 | } else { |
| 321 | entry |
| 322 | }); |
| 323 | } |
| 324 | Ok(Message::Send { entries: out }) |
| 325 | } |
| 326 | } |
| 327 | |
| 328 | |
| 329 | /// Replaces the veiled entries of an arriving message with stand-ins, keeping |
| 330 | /// each veiled entry by identifier. |
| 331 | /// |
| 332 | /// What a carrier does instead of reading. The session that follows sees a log of |
| 333 | /// records and asks it only for headers, and the veiled entries are what goes |
| 334 | /// back on the wire and to the disk in their place. |
| 335 | pub fn placehold(msg: Message, kept: &mut BTreeMap<OpId, Veiled>) |
| 336 | -> Outcome<Message> |
| 337 | { |
| 338 | let entries = match msg { |
| 339 | Message::Send { entries } => entries, |
| 340 | other => return Ok(other), |
| 341 | }; |
| 342 | let mut out = Vec::with_capacity(entries.len()); |
| 343 | for entry in entries { |
| 344 | match entry { |
| 345 | Entry::Veiled(v) => { |
| 346 | kept.insert(v.head.id(), v.clone()); |
| 347 | out.push(Entry::Bare(standin(v.head))); |
| 348 | }, |
| 349 | other => out.push(other), |
| 350 | } |
| 351 | } |
| 352 | Ok(Message::Send { entries: out }) |
| 353 | } |
| 354 | |
| 355 | /// Puts the veiled entries back in place of the stand-ins held for them. |
| 356 | /// |
| 357 | /// The mirror of [`placehold`], and the only way a stand-in is allowed to leave |
| 358 | /// the carrier. An entry whose identifier is not one of them is passed through: |
| 359 | /// a repository that veils holds plain operations too, written before it did. |
| 360 | pub fn restore(entries: Vec<Entry>, kept: &BTreeMap<OpId, Veiled>) |
| 361 | -> Outcome<Vec<Entry>> |
| 362 | { |
| 363 | let mut out = Vec::with_capacity(entries.len()); |
| 364 | for entry in entries { |
| 365 | out.push(res!(restore_one(entry, kept))); |
| 366 | } |
| 367 | Ok(out) |
| 368 | } |
| 369 | |
| 370 | /// The same substitution, one entry at a time. |
| 371 | /// |
| 372 | /// The mirror of [`ore_store::store::seal_one`], and it exists for the same |
| 373 | /// caller: a relay that stops building its reply once it has the reply bound's |
| 374 | /// worth never looks at the entries past it, so the substitutions cannot be done |
| 375 | /// over the set entire. |
| 376 | pub fn restore_one(entry: Entry, kept: &BTreeMap<OpId, Veiled>) |
| 377 | -> Outcome<Entry> |
| 378 | { |
| 379 | let id = res!(entry.id()); |
| 380 | Ok(match kept.get(&id) { |
| 381 | Some(v) => Entry::Veiled(v.clone()), |
| 382 | None => entry, |
| 383 | }) |
| 384 | } |
| 385 | |
| 386 | |
| 387 | #[cfg(test)] |
| 388 | mod tests { |
| 389 | use super::*; |
| 390 | |
| 391 | use oxedyne_fe2o3_ore::id::ReplicaId; |
| 392 | |
| 393 | fn oid(replica: u64, counter: u64) -> OpId { |
| 394 | OpId::new(ReplicaId::new(replica), counter) |
| 395 | } |
| 396 | |
| 397 | /// A key survives the file form it is written in, and one of the wrong length |
| 398 | /// is refused in words a person can act on. |
| 399 | /// |
| 400 | /// The cipher refuses it too, one call further in, and says only that a slice |
| 401 | /// would not become an array. What is asserted here is therefore the sentence |
| 402 | /// and not the refusal: somebody who pasted half a key needs to be told that it |
| 403 | /// is half a key and where the whole one comes from. |
| 404 | #[test] |
| 405 | fn a_content_key_is_taken_or_it_is_not() -> Outcome<()> { |
| 406 | let veil = res!(Veil::mint()); |
| 407 | let text = veil.text(); |
| 408 | let again = res!(Veil::of(&res!(bytes_of(&text)))); |
| 409 | assert_eq!(again.text(), text); |
| 410 | for bad in [0usize, 16, 31, 33] { |
| 411 | let refused = match Veil::of(&vec![7u8; bad]) { |
| 412 | Ok(_) => return Err(err!( |
| 413 | "A content key of {} bytes was accepted.", bad; Test, Invalid)), |
| 414 | Err(e) => fmt!("{}", e.plain()), |
| 415 | }; |
| 416 | assert!(refused.contains(&fmt!("{} bytes and this one is {}", KEY_LEN, bad)), |
| 417 | "the refusal does not say how long it should have been: {}", refused); |
| 418 | assert!(refused.contains("ore key --veil"), |
| 419 | "nor where a whole one comes from: {}", refused); |
| 420 | } |
| 421 | Ok(()) |
| 422 | } |
| 423 | |
| 424 | /// A stand-in is recognised by the name it carries, and an ordinary mark of |
| 425 | /// the same shape is not mistaken for one. |
| 426 | #[test] |
| 427 | fn a_standin_says_what_it_is() -> Outcome<()> { |
| 428 | let head = res!(Header::new(oid(2, 3), vec![oid(1, 1)])); |
| 429 | let rec = standin(head.clone()); |
| 430 | assert!(is_standin(&rec)); |
| 431 | assert_eq!(rec.head, head, "a stand-in carries the true header"); |
| 432 | assert!(fmt!("{:?}", rec.op).contains("r2:3"), |
| 433 | "and says which operation it stands for: {:?}", rec.op); |
| 434 | // The same name against another operation is not that operation's |
| 435 | // stand-in, since the name carries the identifier. |
| 436 | let borrowed = Record::new( |
| 437 | res!(Header::new(oid(2, 4), vec![oid(1, 1)])), |
| 438 | rec.op.clone(), |
| 439 | ); |
| 440 | assert!(!is_standin(&borrowed)); |
| 441 | let ordinary = Record::root(oid(1, 1), Op::Mark { |
| 442 | name: fmt!("release 1.0"), |
| 443 | body: None, |
| 444 | time: None, |
| 445 | }); |
| 446 | assert!(!is_standin(&ordinary)); |
| 447 | Ok(()) |
| 448 | } |
| 449 | |
| 450 | /// Every entry a message carries goes under the veil and comes back out of |
| 451 | /// it, and a message that carries none is left alone. |
| 452 | #[test] |
| 453 | fn a_message_veils_and_unveils_whole() -> Outcome<()> { |
| 454 | let veil = res!(Veil::mint()); |
| 455 | let secret: &[u8] = b"the merger closes on Friday"; |
| 456 | let entries = vec![ |
| 457 | Entry::Bare(Record::root(oid(1, 1), Op::FileCreate { |
| 458 | path: secret.to_vec(), |
| 459 | })), |
| 460 | Entry::Bare(Record::new( |
| 461 | res!(Header::new(oid(1, 2), vec![oid(1, 1)])), |
| 462 | Op::Mark { name: fmt!("start"), body: None, time: None }, |
| 463 | )), |
| 464 | ]; |
| 465 | let sent = res!(veil.veil(Message::Send { entries: entries.clone() })); |
| 466 | for entry in sent.entries() { |
| 467 | assert!(entry.is_veiled()); |
| 468 | } |
| 469 | let bytes = res!(sent.encode()); |
| 470 | assert!( |
| 471 | !bytes.windows(secret.len()).any(|w| w == secret), |
| 472 | "the message on the wire carries the operation's content", |
| 473 | ); |
| 474 | let back = res!(veil.unveil(sent)); |
| 475 | assert_eq!(back.entries(), &entries[..]); |
| 476 | // Anything that is not a batch of operations passes through untouched. |
| 477 | assert_eq!(res!(veil.veil(Message::Done)), Message::Done); |
| 478 | assert_eq!(res!(veil.unveil(Message::Done)), Message::Done); |
| 479 | Ok(()) |
| 480 | } |
| 481 | |
| 482 | /// A carrier places the veiled entries by stand-in and hands back exactly |
| 483 | /// what it was given. |
| 484 | #[test] |
| 485 | fn a_carrier_stands_in_and_hands_back_the_veil() -> Outcome<()> { |
| 486 | let veil = res!(Veil::mint()); |
| 487 | let entries = vec![ |
| 488 | Entry::Bare(Record::root(oid(1, 1), Op::FileCreate { |
| 489 | path: b"notes.md".to_vec(), |
| 490 | })), |
| 491 | Entry::Bare(Record::new( |
| 492 | res!(Header::new(oid(1, 2), vec![oid(1, 1)])), |
| 493 | Op::Mark { name: fmt!("start"), body: None, time: None }, |
| 494 | )), |
| 495 | ]; |
| 496 | let sent = res!(veil.veil(Message::Send { entries: entries.clone() })); |
| 497 | let mut kept: BTreeMap<OpId, Veiled> = BTreeMap::new(); |
| 498 | let held = res!(placehold(sent.clone(), &mut kept)); |
| 499 | assert_eq!(kept.len(), 2); |
| 500 | for entry in held.entries() { |
| 501 | let rec = res!(entry.peek()); |
| 502 | assert!(is_standin(&rec), "a carrier holds a stand-in, not an operation"); |
| 503 | } |
| 504 | // The graph the carrier placed them in is the true one. |
| 505 | let placed: Vec<OpId> = held.entries().iter() |
| 506 | .map(|e| match e.id() { Ok(id) => id, Err(_) => oid(0, 0) }) |
| 507 | .collect(); |
| 508 | assert_eq!(placed, vec![oid(1, 1), oid(1, 2)]); |
| 509 | assert_eq!(res!(held.entries()[1].peek()).parents(), vec![oid(1, 1)]); |
| 510 | // And what it hands back is what it was given, byte for byte. |
| 511 | let out = res!(restore(held.entries().to_vec(), &kept)); |
| 512 | assert_eq!(out, sent.entries().to_vec()); |
| 513 | assert_eq!(res!(veil.unveil(Message::Send { entries: out })).entries(), &entries[..]); |
| 514 | Ok(()) |
| 515 | } |
| 516 | } |