OXYGENHUB
# 2b5ee371060e oxygen / crates / oxy-core / src / protocol.rs
20.6 KiB · Rust · last changed TLS: oxy serve --tls-cert/--tls-key via rustls; remote.<name>.ca and OXY_CA for self-hosted roots 1 month ago source blame history raw
5b9c6ad4 Andy McCall ↶ 1
//! Transport-agnostic protocol (spec §16): what a client asks of a remote and
2
//! what a server does with a push. `LocalRemote` serves another repository
750251b2 Andy McCall ↶ 3
//! directory in-process, `HttpRemote` speaks to an `oxy-server` over HTTP(S),
4
//! and the server side reuses `Repo::receive_push` and friends unchanged.
5
//!
6
//! Wire messages are deterministic CBOR (`wire` module); over HTTP each is a
7
//! request or response body with content type `application/oxy-cbor`.
5b9c6ad4 Andy McCall ↶ 8
9
use crate::attest::Attest;
750251b2 Andy McCall ↶ 10
use crate::cbor::{self, Value};
5b9c6ad4 Andy McCall ↶ 11
use crate::hash::{ChangeHash, ObjectRef, ObjectType};
12
use crate::identity::Signature;
750251b2 Andy McCall ↶ 13
use crate::objects::{self as o, Change, ObjectError};
5b9c6ad4 Andy McCall ↶ 14
use crate::repo::{Repo, RepoError, Result};
2b5ee371 Andy McCall ↶ 15
use std::path::{Path, PathBuf};
16
use std::sync::Arc;
5b9c6ad4 Andy McCall ↶ 17
750251b2 Andy McCall ↶ 18
pub const CONTENT_TYPE: &str = "application/oxy-cbor";
19
pub const PROTOCOL_VERSION: u64 = 1;
20
5b9c6ad4 Andy McCall ↶ 21
/// An attestation together with the signatures over its ObjectRef.
22
#[derive(Clone, Debug, PartialEq, Eq)]
23
pub struct SignedAttest {
24
    pub attest: Attest,
25
    pub sigs: Vec<Signature>,
26
}
27
28
/// One reference as a remote reports it.
29
#[derive(Clone, Debug)]
30
pub struct RemoteRef {
31
    pub name: String,
32
    pub state: ObjectRef,
33
    pub heads: Vec<ChangeHash>,
34
    pub attestation: Option<SignedAttest>,
35
}
36
37
/// Everything needed to add a set of changes to a repository.
38
#[derive(Clone, Debug, Default)]
39
pub struct Bundle {
40
    /// In dependency order, each with its individual signatures.
41
    pub changes: Vec<(Change, Vec<Signature>)>,
42
    /// Seals covering the changes.
43
    pub attests: Vec<SignedAttest>,
44
    /// Chunks, blobs, identities, and state objects referenced by the changes.
45
    pub objects: Vec<(ObjectType, Vec<u8>)>,
46
}
47
48
impl Bundle {
49
    pub fn is_empty(&self) -> bool {
50
        self.changes.is_empty() && self.attests.is_empty() && self.objects.is_empty()
51
    }
52
}
53
54
/// A requested bookmark move (spec §10.2, §16.3).
55
#[derive(Clone, Debug)]
56
pub struct RefUpdate {
57
    pub name: String,
58
    pub old_state: ObjectRef,
59
    pub new_state: ObjectRef,
60
    pub force: bool,
61
    pub attestation: Option<SignedAttest>,
62
}
63
64
#[derive(Clone, Debug, PartialEq, Eq)]
65
pub struct RefResult {
66
    pub name: String,
67
    pub ok: bool,
68
    pub reason: String,
69
}
70
71
pub trait Remote {
72
    fn repo_id(&self) -> Result<[u8; 16]>;
73
    fn refs(&self) -> Result<Vec<RemoteRef>>;
74
    /// Changes in the closure of `want` but not of `have`, in dependency order.
75
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>>;
76
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle>;
77
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>>;
78
}
79
750251b2 Andy McCall ↶ 80
// ---------------------------------------------------------------- local
81
5b9c6ad4 Andy McCall ↶ 82
/// Another repository on this machine, served in-process.
83
pub struct LocalRemote {
84
    pub repo: Repo,
85
}
86
87
impl LocalRemote {
88
    pub fn open(path: &Path) -> Result<LocalRemote> {
89
        Ok(LocalRemote { repo: Repo::open(path)? })
90
    }
91
}
92
93
impl Remote for LocalRemote {
94
    fn repo_id(&self) -> Result<[u8; 16]> {
95
        self.repo.id()
96
    }
97
    fn refs(&self) -> Result<Vec<RemoteRef>> {
98
        self.repo.remote_refs()
99
    }
100
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
101
        self.repo.negotiate(have, want)
102
    }
103
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle> {
104
        self.repo.bundle_for(changes)
105
    }
106
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>> {
107
        self.repo.receive_push(bundle, updates)
108
    }
109
}
110
750251b2 Andy McCall ↶ 111
// ---------------------------------------------------------------- http
112
2b5ee371 Andy McCall ↶ 113
/// How to reach a remote: credentials and trust.
114
#[derive(Clone, Debug, Default)]
115
pub struct RemoteOptions {
116
    /// Bearer token for pushes.
117
    pub token: Option<String>,
118
    /// PEM file with extra root certificates to trust for `https://` remotes
119
    /// (self-hosted servers); the system web roots are always trusted.
120
    pub ca: Option<PathBuf>,
121
}
122
750251b2 Andy McCall ↶ 123
/// A remote served by `oxy-server` at `<base>/oxy/v1/...`.
124
pub struct HttpRemote {
125
    base: String,
126
    token: Option<String>,
127
    agent: ureq::Agent,
128
}
129
2b5ee371 Andy McCall ↶ 130
/// A TLS client configuration trusting the web roots plus `ca`, if given.
131
fn tls_client_config(ca: Option<&Path>) -> Result<Arc<rustls::ClientConfig>> {
132
    let mut roots = rustls::RootCertStore::empty();
133
    roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
134
    if let Some(path) = ca {
135
        let pem = std::fs::read(path).map_err(|e| RepoError::Msg(format!("reading CA file {}: {e}", path.display())))?;
136
        let certs = rustls_pemfile::certs(&mut pem.as_slice())
137
            .collect::<std::result::Result<Vec<_>, _>>()
138
            .map_err(|e| RepoError::Msg(format!("parsing CA file {}: {e}", path.display())))?;
139
        if certs.is_empty() {
140
            return Err(RepoError::Msg(format!("CA file {} holds no certificates", path.display())));
141
        }
142
        for c in certs {
143
            roots.add(c).map_err(|e| RepoError::Msg(format!("CA file {}: {e}", path.display())))?;
144
        }
145
    }
146
    let cfg = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
147
        .with_safe_default_protocol_versions()
148
        .map_err(|e| RepoError::Msg(e.to_string()))?
149
        .with_root_certificates(roots)
150
        .with_no_client_auth();
151
    Ok(Arc::new(cfg))
152
}
153
750251b2 Andy McCall ↶ 154
impl HttpRemote {
2b5ee371 Andy McCall ↶ 155
    pub fn new(base: &str, options: &RemoteOptions) -> Result<HttpRemote> {
156
        let agent = ureq::AgentBuilder::new().tls_config(tls_client_config(options.ca.as_deref())?).build();
157
        Ok(HttpRemote { base: base.trim_end_matches('/').to_string(), token: options.token.clone(), agent })
750251b2 Andy McCall ↶ 158
    }
159
160
    fn call(&self, post: bool, endpoint: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
161
        let url = format!("{}/oxy/v1/{endpoint}", self.base);
162
        let mut req = if post { self.agent.post(&url) } else { self.agent.get(&url) };
163
        req = req.set("Accept", CONTENT_TYPE);
164
        if let Some(t) = &self.token {
165
            req = req.set("Authorization", &format!("Bearer {t}"));
166
        }
167
        let resp = match body {
168
            Some(b) => req.set("Content-Type", CONTENT_TYPE).send_bytes(b),
169
            None => req.call(),
170
        };
171
        match resp {
172
            Ok(r) => {
173
                let mut out = Vec::new();
174
                r.into_reader().read_to_end(&mut out).map_err(|e| RepoError::Msg(format!("{url}: {e}")))?;
175
                Ok(out)
176
            }
177
            Err(ureq::Error::Status(code, r)) => {
178
                let text = r.into_string().unwrap_or_default();
179
                Err(RepoError::Msg(format!("{url}: HTTP {code}: {}", text.trim())))
180
            }
181
            Err(e) => Err(RepoError::Msg(format!("{url}: {e}"))),
182
        }
183
    }
184
}
185
186
impl Remote for HttpRemote {
187
    fn repo_id(&self) -> Result<[u8; 16]> {
188
        wire::decode_capabilities(&self.call(false, "capabilities", None)?)
189
    }
190
    fn refs(&self) -> Result<Vec<RemoteRef>> {
191
        Ok(wire::decode_refs(&self.call(false, "refs", None)?)?.1)
192
    }
193
    fn negotiate(&self, have: &[ChangeHash], want: &[ChangeHash]) -> Result<Vec<ChangeHash>> {
194
        wire::decode_hashes(&self.call(true, "negotiate", Some(&wire::encode_negotiate(have, want)))?)
195
    }
196
    fn fetch(&self, changes: &[ChangeHash]) -> Result<Bundle> {
197
        wire::decode_bundle(&self.call(true, "fetch", Some(&wire::encode_hashes(changes)))?)
198
    }
199
    fn push(&self, bundle: Bundle, updates: Vec<RefUpdate>) -> Result<Vec<RefResult>> {
200
        wire::decode_push_response(&self.call(true, "push", Some(&wire::encode_push(&bundle, &updates)))?)
201
    }
202
}
203
204
/// Open a remote by URL: a local path (optionally `file://`), or `http(s)://`.
2b5ee371 Andy McCall ↶ 205
pub fn open_remote(url: &str, options: &RemoteOptions) -> Result<Box<dyn Remote>> {
750251b2 Andy McCall ↶ 206
    if url.starts_with("http://") || url.starts_with("https://") {
2b5ee371 Andy McCall ↶ 207
        return Ok(Box::new(HttpRemote::new(url, options)?));
750251b2 Andy McCall ↶ 208
    }
5b9c6ad4 Andy McCall ↶ 209
    let path = url.strip_prefix("file://").unwrap_or(url);
210
    if path.contains("://") {
750251b2 Andy McCall ↶ 211
        return Err(RepoError::Msg(format!("unsupported remote URL {url:?}")));
5b9c6ad4 Andy McCall ↶ 212
    }
213
    Ok(Box::new(LocalRemote::open(Path::new(path))?))
214
}
750251b2 Andy McCall ↶ 215
216
// ---------------------------------------------------------------- wire
217
218
/// CBOR encodings of the protocol messages (spec §16.1).
219
pub mod wire {
220
    use super::*;
221
222
    fn schema(msg: &str) -> RepoError {
223
        RepoError::Object(ObjectError::Schema(msg.into()))
224
    }
225
    fn sigs_value(sigs: &[Signature]) -> Value {
226
        Value::Array(sigs.iter().map(|s| s.to_value()).collect())
227
    }
228
    fn sigs_from(v: &Value) -> Result<Vec<Signature>> {
229
        Ok(o::array(v, "signatures")?.iter().map(Signature::from_value).collect::<std::result::Result<Vec<_>, _>>()?)
230
    }
231
    fn hashes_value(hs: &[ObjectRef]) -> Value {
232
        Value::Array(hs.iter().map(|h| Value::Bytes(h.0.to_vec())).collect())
233
    }
234
    fn hashes_from(v: &Value) -> Result<Vec<ObjectRef>> {
235
        Ok(o::array(v, "hashes")?.iter().map(|h| o::oref(h, "hash")).collect::<std::result::Result<Vec<_>, _>>()?)
236
    }
237
    fn attest_value(sa: &SignedAttest) -> Value {
238
        Value::Array(vec![Value::Bytes(sa.attest.encode()), sigs_value(&sa.sigs)])
239
    }
240
    fn attest_from(v: &Value) -> Result<SignedAttest> {
241
        let a = o::array(v, "signed attest")?;
242
        if a.len() != 2 {
243
            return Err(schema("signed attest: expected [attest, sigs]"));
244
        }
245
        Ok(SignedAttest { attest: Attest::decode(o::bytes(&a[0], "attest")?)?, sigs: sigs_from(&a[1])? })
246
    }
247
    fn opt_attest_value(sa: &Option<SignedAttest>) -> Value {
248
        sa.as_ref().map(attest_value).unwrap_or(Value::Null)
249
    }
250
    fn opt_attest_from(v: &Value) -> Result<Option<SignedAttest>> {
251
        if v.is_null() { Ok(None) } else { attest_from(v).map(Some) }
252
    }
253
    fn map<'a>(v: &'a Value, what: &str) -> Result<&'a o::Map> {
254
        Ok(o::map(v, what)?)
255
    }
256
    fn field<'a>(m: &'a o::Map, k: u64, what: &str) -> Result<&'a Value> {
257
        Ok(o::field(m, k, what)?)
258
    }
259
260
    pub fn bundle_value(b: &Bundle) -> Value {
261
        Value::map([
262
            (
263
                0,
264
                Value::Array(b.changes.iter().map(|(c, s)| Value::Array(vec![Value::Bytes(c.encode()), sigs_value(s)])).collect()),
265
            ),
266
            (1, Value::Array(b.attests.iter().map(attest_value).collect())),
267
            (
268
                2,
269
                Value::Array(
270
                    b.objects.iter().map(|(t, body)| Value::Array(vec![Value::Text(t.name().into()), Value::Bytes(body.clone())])).collect(),
271
                ),
272
            ),
273
        ])
274
    }
275
276
    pub fn bundle_from_value(v: &Value) -> Result<Bundle> {
277
        let m = map(v, "bundle")?;
278
        let mut b = Bundle::default();
279
        for c in o::array(field(m, 0, "changes")?, "changes")? {
280
            let pair = o::array(c, "change entry")?;
281
            if pair.len() != 2 {
282
                return Err(schema("change entry: expected [change, sigs]"));
283
            }
284
            b.changes.push((Change::decode(o::bytes(&pair[0], "change")?)?, sigs_from(&pair[1])?));
285
        }
286
        for a in o::array(field(m, 1, "attests")?, "attests")? {
287
            b.attests.push(attest_from(a)?);
288
        }
289
        for ob in o::array(field(m, 2, "objects")?, "objects")? {
290
            let pair = o::array(ob, "object")?;
291
            if pair.len() != 2 {
292
                return Err(schema("object: expected [type, body]"));
293
            }
294
            let ty = ObjectType::from_name(o::text(&pair[0], "object type")?).ok_or_else(|| schema("unknown object type"))?;
295
            b.objects.push((ty, o::bytes(&pair[1], "object body")?.to_vec()));
296
        }
297
        Ok(b)
298
    }
299
300
    pub fn encode_bundle(b: &Bundle) -> Vec<u8> {
301
        cbor::encode(&bundle_value(b))
302
    }
303
    pub fn decode_bundle(bytes: &[u8]) -> Result<Bundle> {
304
        bundle_from_value(&cbor::decode(bytes).map_err(ObjectError::from)?)
305
    }
306
307
    pub fn encode_capabilities(id: [u8; 16]) -> Vec<u8> {
308
        cbor::encode(&Value::map([
309
            (0, Value::Array(vec![Value::Uint(PROTOCOL_VERSION)])),
310
            (1, Value::Bytes(id.to_vec())),
311
            (2, Value::Array(vec![Value::Text("seal".into())])),
312
        ]))
313
    }
314
    pub fn decode_capabilities(bytes: &[u8]) -> Result<[u8; 16]> {
315
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
316
        let m = map(&v, "capabilities")?;
317
        let versions = o::array(field(m, 0, "versions")?, "versions")?;
318
        if !versions.iter().any(|x| x.as_uint() == Some(PROTOCOL_VERSION)) {
319
            return Err(RepoError::Msg("remote does not speak protocol version 1".into()));
320
        }
321
        o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))
322
    }
323
324
    pub fn encode_refs(id: [u8; 16], refs: &[RemoteRef]) -> Vec<u8> {
325
        cbor::encode(&Value::map([
326
            (
327
                0,
328
                Value::Array(
329
                    refs.iter()
330
                        .map(|r| {
331
                            Value::map([
332
                                (0, Value::Text(r.name.clone())),
333
                                (1, Value::Bytes(r.state.0.to_vec())),
334
                                (2, hashes_value(&r.heads)),
335
                                (3, opt_attest_value(&r.attestation)),
336
                                (4, r.last_serial.map(Value::Uint).unwrap_or(Value::Null)),
337
                            ])
338
                        })
339
                        .collect(),
340
                ),
341
            ),
342
            (1, Value::Bytes(id.to_vec())),
343
        ]))
344
    }
345
    pub fn decode_refs(bytes: &[u8]) -> Result<([u8; 16], Vec<RemoteRef>)> {
346
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
347
        let m = map(&v, "refs")?;
348
        let id: [u8; 16] = o::bytes(field(m, 1, "repo id")?, "repo id")?.try_into().map_err(|_| schema("repo id: expected 16 bytes"))?;
349
        let mut refs = Vec::new();
350
        for r in o::array(field(m, 0, "refs")?, "refs")? {
351
            let rm = map(r, "ref")?;
352
            let ls = field(rm, 4, "last serial")?;
353
            refs.push(RemoteRef {
354
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
355
                state: o::oref(field(rm, 1, "state")?, "state")?,
356
                heads: hashes_from(field(rm, 2, "heads")?)?,
357
                attestation: opt_attest_from(field(rm, 3, "attestation")?)?,
358
                last_serial: if ls.is_null() { None } else { Some(o::uint(ls, "last serial")?) },
359
            });
360
        }
361
        Ok((id, refs))
362
    }
363
364
    pub fn encode_negotiate(have: &[ChangeHash], want: &[ChangeHash]) -> Vec<u8> {
365
        cbor::encode(&Value::map([(0, hashes_value(have)), (1, hashes_value(want))]))
366
    }
367
    pub fn decode_negotiate(bytes: &[u8]) -> Result<(Vec<ChangeHash>, Vec<ChangeHash>)> {
368
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
369
        let m = map(&v, "negotiate")?;
370
        Ok((hashes_from(field(m, 0, "have")?)?, hashes_from(field(m, 1, "want")?)?))
371
    }
372
373
    pub fn encode_hashes(hs: &[ChangeHash]) -> Vec<u8> {
374
        cbor::encode(&Value::map([(0, hashes_value(hs))]))
375
    }
376
    pub fn decode_hashes(bytes: &[u8]) -> Result<Vec<ChangeHash>> {
377
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
378
        hashes_from(field(map(&v, "hashes")?, 0, "hashes")?)
379
    }
380
381
    pub fn encode_push(bundle: &Bundle, updates: &[RefUpdate]) -> Vec<u8> {
382
        cbor::encode(&Value::map([
383
            (0, bundle_value(bundle)),
384
            (
385
                1,
386
                Value::Array(
387
                    updates
388
                        .iter()
389
                        .map(|u| {
390
                            Value::map([
391
                                (0, Value::Text(u.name.clone())),
392
                                (1, Value::Bytes(u.old_state.0.to_vec())),
393
                                (2, Value::Bytes(u.new_state.0.to_vec())),
394
                                (3, Value::Bool(u.force)),
395
                                (4, opt_attest_value(&u.attestation)),
396
                            ])
397
                        })
398
                        .collect(),
399
                ),
400
            ),
401
        ]))
402
    }
403
    pub fn decode_push(bytes: &[u8]) -> Result<(Bundle, Vec<RefUpdate>)> {
404
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
405
        let m = map(&v, "push")?;
406
        let bundle = bundle_from_value(field(m, 0, "bundle")?)?;
407
        let mut updates = Vec::new();
408
        for u in o::array(field(m, 1, "updates")?, "updates")? {
409
            let um = map(u, "update")?;
410
            updates.push(RefUpdate {
411
                name: o::text(field(um, 0, "name")?, "name")?.to_owned(),
412
                old_state: o::oref(field(um, 1, "old_state")?, "old_state")?,
413
                new_state: o::oref(field(um, 2, "new_state")?, "new_state")?,
414
                force: field(um, 3, "force")?.as_bool().ok_or_else(|| schema("force: expected bool"))?,
415
                attestation: opt_attest_from(field(um, 4, "attestation")?)?,
416
            });
417
        }
418
        Ok((bundle, updates))
419
    }
420
421
    pub fn encode_push_response(results: &[RefResult]) -> Vec<u8> {
422
        cbor::encode(&Value::map([(
423
            0,
424
            Value::Array(
425
                results
426
                    .iter()
427
                    .map(|r| Value::map([(0, Value::Text(r.name.clone())), (1, Value::Bool(r.ok)), (2, Value::Text(r.reason.clone()))]))
428
                    .collect(),
429
            ),
430
        )]))
431
    }
432
    pub fn decode_push_response(bytes: &[u8]) -> Result<Vec<RefResult>> {
433
        let v = cbor::decode(bytes).map_err(ObjectError::from)?;
434
        let m = map(&v, "push response")?;
435
        let mut out = Vec::new();
436
        for r in o::array(field(m, 0, "results")?, "results")? {
437
            let rm = map(r, "result")?;
438
            out.push(RefResult {
439
                name: o::text(field(rm, 0, "name")?, "name")?.to_owned(),
440
                ok: field(rm, 1, "ok")?.as_bool().ok_or_else(|| schema("ok: expected bool"))?,
441
                reason: o::text(field(rm, 2, "reason")?, "reason")?.to_owned(),
442
            });
443
        }
444
        Ok(out)
445
    }
446
}
447
448
#[cfg(test)]
449
mod tests {
450
    use super::*;
451
    use crate::attest::AttestBody;
452
    use crate::objects::Hlc;
453
454
    #[test]
455
    fn wire_round_trips() {
456
        let c = crate::objects::tests::sample_change();
457
        let sk = crate::identity::SecretKey { ed_seed: [1; 32], ml_seed: [2; 32] };
458
        let id = sk.identity("a", "b", 0);
459
        let key = id.hash();
460
        let sig = sk.sign(crate::identity::Domain::Change, &key, &c.hash());
461
        let seal = Attest::seal([3; 16], vec![c.hash()]);
462
        let seal_sig = sk.sign(crate::identity::Domain::Attest, &key, &seal.hash());
463
        let bundle = Bundle {
464
            changes: vec![(c.clone(), vec![sig.clone()])],
465
            attests: vec![SignedAttest { attest: seal.clone(), sigs: vec![seal_sig] }],
466
            objects: vec![(ObjectType::Identity, id.encode()), (ObjectType::Chunk, b"xyz".to_vec())],
467
        };
468
        let d = wire::decode_bundle(&wire::encode_bundle(&bundle)).unwrap();
469
        assert_eq!(d.changes, bundle.changes);
470
        assert_eq!(d.attests, bundle.attests);
471
        assert_eq!(d.objects, bundle.objects);
472
473
        let att = Attest { repo: [3; 16], hlc: Hlc::default(), body: AttestBody::Ref { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), serial: 4 } };
474
        let refs = vec![
475
            RemoteRef { name: "main".into(), state: ObjectRef([2; 32]), heads: vec![c.hash()], attestation: Some(SignedAttest { attest: att.clone(), sigs: vec![sig.clone()] }), last_serial: Some(4) },
476
            RemoteRef { name: "dev".into(), state: ObjectRef([5; 32]), heads: vec![], attestation: None, last_serial: None },
477
        ];
478
        let (id_back, back) = wire::decode_refs(&wire::encode_refs([3; 16], &refs)).unwrap();
479
        assert_eq!(id_back, [3; 16]);
480
        assert_eq!(back.len(), 2);
481
        assert_eq!(back[0].attestation.as_ref().unwrap().attest, att);
482
        assert_eq!(back[1].last_serial, None);
483
484
        let (h, w) = wire::decode_negotiate(&wire::encode_negotiate(&[ObjectRef([1; 32])], &[ObjectRef([2; 32])])).unwrap();
485
        assert_eq!((h, w), (vec![ObjectRef([1; 32])], vec![ObjectRef([2; 32])]));
486
        assert_eq!(wire::decode_hashes(&wire::encode_hashes(&[c.hash()])).unwrap(), vec![c.hash()]);
487
488
        let up = RefUpdate { name: "main".into(), old_state: ObjectRef([1; 32]), new_state: ObjectRef([2; 32]), force: true, attestation: Some(SignedAttest { attest: att, sigs: vec![sig] }) };
489
        let (b2, ups) = wire::decode_push(&wire::encode_push(&bundle, std::slice::from_ref(&up))).unwrap();
490
        assert_eq!(b2.changes.len(), 1);
491
        assert_eq!(ups[0].name, "main");
492
        assert!(ups[0].force);
493
        assert_eq!(ups[0].attestation, up.attestation);
494
        let res = vec![RefResult { name: "main".into(), ok: false, reason: "stale".into() }];
495
        assert_eq!(wire::decode_push_response(&wire::encode_push_response(&res)).unwrap(), res);
496
        assert_eq!(wire::decode_capabilities(&wire::encode_capabilities([7; 16])).unwrap(), [7; 16]);
497
    }
498
}