2026-05-16 14:37:16 +02:00
|
|
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
|
|
|
|
|
|
|
|
|
pub const CANONICAL_ENVELOPE_VERSION: u16 = 1;
|
2026-05-15 15:08:20 +02:00
|
|
|
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum CodecError {
|
|
|
|
|
#[error("canonical encoding failed: {0}")]
|
|
|
|
|
Encode(#[from] postcard::Error),
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 14:37:16 +02:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct CanonicalEnvelope<T> {
|
|
|
|
|
pub version: u16,
|
|
|
|
|
pub namespace: String,
|
|
|
|
|
pub payload: T,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T> CanonicalEnvelope<T> {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn new(namespace: impl Into<String>, payload: T) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
version: CANONICAL_ENVELOPE_VERSION,
|
|
|
|
|
namespace: namespace.into(),
|
|
|
|
|
payload,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
pub struct SignedEnvelope<T, S> {
|
|
|
|
|
pub envelope: CanonicalEnvelope<T>,
|
|
|
|
|
pub signer: S,
|
|
|
|
|
pub signature: Vec<u8>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<T, S> SignedEnvelope<T, S> {
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn new(namespace: impl Into<String>, payload: T, signer: S, signature: Vec<u8>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
envelope: CanonicalEnvelope::new(namespace, payload),
|
|
|
|
|
signer,
|
|
|
|
|
signature,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn namespace(&self) -> &str {
|
|
|
|
|
&self.envelope.namespace
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn payload(&self) -> &T {
|
|
|
|
|
&self.envelope.payload
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct CanonicalEnvelopeRef<'a, T: ?Sized> {
|
|
|
|
|
version: u16,
|
|
|
|
|
namespace: &'a str,
|
|
|
|
|
payload: &'a T,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 15:08:20 +02:00
|
|
|
pub fn encode_canonical<T: Serialize + ?Sized>(value: &T) -> Result<Vec<u8>, CodecError> {
|
|
|
|
|
postcard::to_allocvec(value).map_err(CodecError::from)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn decode_canonical<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
|
|
|
|
|
postcard::from_bytes(bytes).map_err(CodecError::from)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn hash_canonical<T: Serialize + ?Sized>(
|
|
|
|
|
value: &T,
|
|
|
|
|
) -> Result<geth_types::BlobHash, CodecError> {
|
|
|
|
|
let bytes = encode_canonical(value)?;
|
|
|
|
|
Ok(blake3_hash_bytes(&bytes))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn blake3_hash_bytes(bytes: &[u8]) -> geth_types::BlobHash {
|
|
|
|
|
geth_types::BlobHash::new(blake3::hash(bytes).to_hex().to_string())
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 14:37:16 +02:00
|
|
|
pub fn signing_payload<T: Serialize + ?Sized>(
|
|
|
|
|
namespace: &str,
|
|
|
|
|
payload: &T,
|
|
|
|
|
) -> Result<Vec<u8>, CodecError> {
|
|
|
|
|
encode_canonical(&CanonicalEnvelopeRef {
|
|
|
|
|
version: CANONICAL_ENVELOPE_VERSION,
|
|
|
|
|
namespace,
|
|
|
|
|
payload,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn signing_payload_hash<T: Serialize + ?Sized>(
|
|
|
|
|
namespace: &str,
|
|
|
|
|
payload: &T,
|
|
|
|
|
) -> Result<geth_types::BlobHash, CodecError> {
|
|
|
|
|
Ok(blake3_hash_bytes(&signing_payload(namespace, payload)?))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 15:08:20 +02:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
struct Sample {
|
|
|
|
|
version: u8,
|
|
|
|
|
name: String,
|
|
|
|
|
values: Vec<u16>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn canonical_encoding_is_deterministic() {
|
|
|
|
|
let sample = Sample {
|
|
|
|
|
version: 1,
|
|
|
|
|
name: "geth".to_owned(),
|
|
|
|
|
values: vec![1, 2, 3],
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
encode_canonical(&sample).expect("encode"),
|
|
|
|
|
encode_canonical(&sample).expect("encode again")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
hash_canonical(&sample).expect("hash"),
|
|
|
|
|
hash_canonical(&sample).expect("hash again")
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
decode_canonical::<Sample>(&encode_canonical(&sample).expect("encode"))
|
|
|
|
|
.expect("decode"),
|
|
|
|
|
sample
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-16 14:37:16 +02:00
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn signing_payload_includes_namespace_and_version() {
|
|
|
|
|
let sample = Sample {
|
|
|
|
|
version: 1,
|
|
|
|
|
name: "geth".to_owned(),
|
|
|
|
|
values: vec![1, 2, 3],
|
|
|
|
|
};
|
|
|
|
|
let keychain_payload =
|
|
|
|
|
signing_payload("geth.keychain.v1@geth.local", &sample).expect("encode");
|
|
|
|
|
let auth_payload = signing_payload("geth.auth-op.v1@geth.local", &sample).expect("encode");
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
keychain_payload,
|
|
|
|
|
signing_payload("geth.keychain.v1@geth.local", &sample).expect("encode again")
|
|
|
|
|
);
|
|
|
|
|
assert_ne!(keychain_payload, auth_payload);
|
|
|
|
|
|
|
|
|
|
let decoded: CanonicalEnvelope<Sample> =
|
|
|
|
|
decode_canonical(&keychain_payload).expect("decode envelope");
|
|
|
|
|
assert_eq!(decoded.version, CANONICAL_ENVELOPE_VERSION);
|
|
|
|
|
assert_eq!(decoded.namespace, "geth.keychain.v1@geth.local");
|
|
|
|
|
assert_eq!(decoded.payload, sample);
|
|
|
|
|
}
|
2026-05-15 15:08:20 +02:00
|
|
|
}
|