Skip to content

Commit 848e2b4

Browse files
committed
Move tagged_blob macro here from jf-utils
It conceptually belongs here anyways. Also, this fixes the dependency tree so that `commit` can depend on `tagged_blob` without depending on `jf-utils` (which would create a cycle). This will enable tagged base 64 serialization support directly on the `Commitment` type, obviating the need for many newtype structs. That will be done in a subsequent change. During the move, I streamlined the `tagged_blob` macro a bit. I removed the `TaggedBlob` wrapper type which was only used to add custom serde impls and to convert to and from for serializing other types. Instead, serialization now goes directly through `TaggedBase64`, and I have implemented serialization and deserialization for the `TaggedBase64` type itself. Since `TaggedBlob` is gone, I renamed the macro `tagged` for simplicity.
1 parent d1632c0 commit 848e2b4

File tree

5 files changed

+311
-34
lines changed

5 files changed

+311
-34
lines changed

Cargo.toml

+8-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@ wasm-debug = ["dep:console_error_panic_hook"]
2020
build-cli = ["dep:clap"]
2121

2222
[dependencies]
23-
crc-any = { version = "2.4.1", default-features = false }
24-
23+
ark-serialize = { version = "0.3.0", default-features = false, features = ["derive"] }
2524
base64 = "0.13.0"
25+
crc-any = { version = "2.4.1", default-features = false }
26+
serde = { version = "1.0", features = ["derive"] }
27+
snafu = { version = "0.7", features = ["backtraces"] }
28+
tagged-base64-macros = { path = "macros" }
2629

2730
# Command line argument processing
2831
clap = { version = "4.0", optional = true, features = ["derive"] }
@@ -40,9 +43,12 @@ web-sys = { version = "0.3.49", optional = true, features = ["console", "Headers
4043
console_error_panic_hook = { version = "0.1.7", optional = true }
4144

4245
[dev-dependencies]
46+
ark-std = { version = "0.3.0", default-features = false }
47+
bincode = "1.3"
4348
getrandom = { version = "0.2", features = ["js"] }
4449
quickcheck = "1.0"
4550
quickcheck_macros = "1.0"
51+
serde_json = "1.0"
4652
wasm-bindgen-test = { version = "0.3.28" }
4753

4854
[profile.release]

macros/Cargo.toml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "tagged-base64-macros"
3+
description = "Procedural macros associated with tagged-base64"
4+
version = "0.2.0"
5+
authors = ["Espresso Systems <hello@espressosys.com>"]
6+
edition = "2018"
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
ark-std = { version = "0.3.0", default-features = false }
13+
syn = { version = "1.0", features = ["extra-traits"] }
14+
quote = "1.0"
15+
16+
[dev-dependencies]
17+
ark-serialize = { version = "0.3.0", default-features = false, features = ["derive"] }
18+
ark-bls12-381 = { version = "0.3.0", default-features = false, features = ["curve"] }
19+
bincode = { version = "1.3.3", default-features = false }
20+
rand_chacha = { version = "0.3.1" }
21+
serde = { version = "1.0", features = ["derive"] }
22+
serde_json = "1.0.61"

macros/src/lib.rs

+156
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
// Copyright (c) 2022 Espresso Systems (espressosys.com)
2+
#![no_std]
3+
4+
extern crate proc_macro;
5+
6+
use proc_macro::TokenStream;
7+
use quote::quote;
8+
use syn::{parse_macro_input, AttributeArgs, Item, Meta, NestedMeta};
9+
10+
/// Derive serdes for a type which serializes as a binary blob.
11+
///
12+
/// This macro can be used to easily derive friendly serde implementations for a binary type which
13+
/// implements [CanonicalSerialize](ark_serialize::CanonicalSerialize) and
14+
/// [CanonicalDeserialize](ark_serialize::CanonicalDeserialize). This is useful for cryptographic
15+
/// primitives and other types which do not have a human-readable serialization, but which may be
16+
/// embedded in structs with a human-readable serialization. The serde implementations derived by
17+
/// this macro will serialize the type as bytes for binary encodings and as base 64 for human
18+
/// readable encodings.
19+
///
20+
/// Specifically, this macro does 4 things when applied to a type definition:
21+
/// * It adds `#[derive(Serialize, Deserialize)]` to the type definition, along with serde
22+
/// attributes to serialize using [TaggedBase64].
23+
/// * It creates an implementation of [Tagged] for the type using the specified tag. This tag will
24+
/// be used to identify base 64 strings which represent this type in human-readable encodings.
25+
/// * It creates an implementation of `TryFrom<TaggedBase64>` for the type `T`, which is needed to
26+
/// make the `serde(try_from)` attribute work.
27+
/// * It creates implementations of [Display](ark_std::fmt::Display) and
28+
/// [FromStr](ark_std::str::FromStr) using tagged base 64 as a display format. This allows tagged
29+
/// blob types to be conveniently displayed and read to and from user interfaces in a manner
30+
/// consistent with how they are serialized.
31+
///
32+
/// Usage example:
33+
///
34+
/// ```
35+
/// #[macro_use] extern crate tagged_base64_macros;
36+
/// use ark_serialize::*;
37+
///
38+
/// #[tagged("PRIM")]
39+
/// #[derive(Clone, CanonicalSerialize, CanonicalDeserialize, /* any other derives */)]
40+
/// pub struct CryptoPrim(
41+
/// // This type can only be serialied as an opaque, binary blob using ark_serialize.
42+
/// pub(crate) ark_bls12_381::Fr,
43+
/// );
44+
/// ```
45+
///
46+
/// The type `CryptoPrim` can now be serialized as binary:
47+
/// ```
48+
/// # use ark_serialize::*;
49+
/// # use ark_std::UniformRand;
50+
/// # use tagged_base64_macros::tagged;
51+
/// # use rand_chacha::{ChaChaRng, rand_core::SeedableRng};
52+
/// # #[tagged("PRIM")]
53+
/// # #[derive(Clone, CanonicalSerialize, CanonicalDeserialize, /* any other derives */)]
54+
/// # struct CryptoPrim(ark_bls12_381::Fr);
55+
/// # let crypto_prim = CryptoPrim(ark_bls12_381::Fr::rand(&mut ChaChaRng::from_seed([42; 32])));
56+
/// bincode::serialize(&crypto_prim).unwrap();
57+
/// ```
58+
/// or as base64:
59+
/// ```
60+
/// # use ark_serialize::*;
61+
/// # use ark_std::UniformRand;
62+
/// # use tagged_base64_macros::tagged;
63+
/// # use rand_chacha::{ChaChaRng, rand_core::SeedableRng};
64+
/// # #[tagged("PRIM")]
65+
/// # #[derive(Clone, CanonicalSerialize, CanonicalDeserialize, /* any other derives */)]
66+
/// # struct CryptoPrim(ark_bls12_381::Fr);
67+
/// # let crypto_prim = CryptoPrim(ark_bls12_381::Fr::rand(&mut ChaChaRng::from_seed([42; 32])));
68+
/// serde_json::to_string(&crypto_prim).unwrap();
69+
/// ```
70+
/// which will produce a tagged base64 string like
71+
/// "PRIM~8oaujwbov8h4eEq7HFpqW6mIXhVbtJGxLUgiKrGpMCoJ".
72+
#[proc_macro_attribute]
73+
pub fn tagged(args: TokenStream, input: TokenStream) -> TokenStream {
74+
let args = parse_macro_input!(args as AttributeArgs);
75+
let input = parse_macro_input!(input as Item);
76+
let (name, generics) = match &input {
77+
Item::Struct(item) => (&item.ident, &item.generics),
78+
Item::Enum(item) => (&item.ident, &item.generics),
79+
_ => panic!("expected struct or enum"),
80+
};
81+
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
82+
let tag: &dyn quote::ToTokens = match args.as_slice() {
83+
[NestedMeta::Lit(tag)] => tag,
84+
[NestedMeta::Meta(Meta::Path(path))] => path,
85+
x => panic!(
86+
"`tagged` takes one argument, the tag, as a string literal or expression, found {:?}",
87+
x
88+
),
89+
};
90+
let output = quote! {
91+
#[derive(serde::Serialize, serde::Deserialize)]
92+
#[serde(try_from = "tagged_base64::TaggedBase64", into = "tagged_base64::TaggedBase64")]
93+
// Override the inferred bound for Serialize/Deserialize impls. If we're converting to and
94+
// from CanonicalBytes as an intermediate, the impls should work for any generic parameters.
95+
#[serde(bound = "")]
96+
#input
97+
98+
impl #impl_generics tagged_base64::Tagged for #name #ty_generics #where_clause {
99+
fn tag() -> ark_std::string::String {
100+
ark_std::string::String::from(#tag)
101+
}
102+
}
103+
104+
impl #impl_generics core::convert::TryFrom<tagged_base64::TaggedBase64>
105+
for #name #ty_generics
106+
#where_clause
107+
{
108+
type Error = tagged_base64::Tb64Error;
109+
fn try_from(t: tagged_base64::TaggedBase64) -> Result<Self, Self::Error> {
110+
if t.tag() == <#name #ty_generics>::tag() {
111+
<Self as CanonicalDeserialize>::deserialize(t.as_ref())
112+
.map_err(|_| tagged_base64::Tb64Error::InvalidData)
113+
} else {
114+
Err(tagged_base64::Tb64Error::InvalidTag)
115+
}
116+
}
117+
}
118+
119+
impl #impl_generics core::convert::From<#name #ty_generics> for tagged_base64::TaggedBase64
120+
#where_clause
121+
{
122+
fn from(x: #name #ty_generics) -> Self {
123+
(&x).into()
124+
}
125+
}
126+
127+
impl #impl_generics core::convert::From<&#name #ty_generics> for tagged_base64::TaggedBase64
128+
#where_clause
129+
{
130+
fn from(x: &#name #ty_generics) -> Self {
131+
let mut bytes = ark_std::vec![];
132+
x.serialize(&mut bytes).unwrap();
133+
Self::new(&<#name #ty_generics>::tag(), &bytes).unwrap()
134+
}
135+
}
136+
137+
impl #impl_generics ark_std::fmt::Display for #name #ty_generics #where_clause {
138+
fn fmt(&self, f: &mut ark_std::fmt::Formatter<'_>) -> ark_std::fmt::Result {
139+
ark_std::write!(
140+
f, "{}",
141+
tagged_base64::TaggedBase64::from(self)
142+
)
143+
}
144+
}
145+
146+
impl #impl_generics ark_std::str::FromStr for #name #ty_generics #where_clause {
147+
type Err = tagged_base64::Tb64Error;
148+
fn from_str(s: &str) -> Result<Self, Self::Err> {
149+
use core::convert::TryFrom;
150+
Self::try_from(tagged_base64::TaggedBase64::from_str(s)?)
151+
.map_err(|_| tagged_base64::Tb64Error::InvalidData)
152+
}
153+
}
154+
};
155+
output.into()
156+
}

src/lib.rs

+77-26
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,23 @@
4141
//! well as display and input in a user interface.
4242
4343
#![allow(clippy::unused_unit)]
44+
use ark_serialize::*;
4445
use core::fmt;
4546
#[cfg(target_arch = "wasm32")]
4647
use core::fmt::Display;
4748
use core::str::FromStr;
4849
use crc_any::CRC;
50+
use serde::{
51+
de::{Deserialize, Deserializer, Error as DeError},
52+
ser::{Error as SerError, Serialize, Serializer},
53+
};
54+
use snafu::Snafu;
4955

5056
#[cfg(target_arch = "wasm32")]
5157
use wasm_bindgen::prelude::*;
5258

59+
pub use tagged_base64_macros::tagged;
60+
5361
/// Separator that does not appear in URL-safe base64 encoding and can
5462
/// appear in URLs without percent-encoding.
5563
pub const TB64_DELIM: char = '~';
@@ -60,13 +68,49 @@ pub const TB64_CONFIG: base64::Config = base64::URL_SAFE_NO_PAD;
6068
/// A structure holding a string tag, vector of bytes, and a checksum
6169
/// covering the tag and the bytes.
6270
#[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
63-
#[derive(Clone, Debug, Eq, PartialEq)]
71+
#[derive(Clone, Debug, Eq, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
6472
pub struct TaggedBase64 {
6573
tag: String,
6674
value: Vec<u8>,
6775
checksum: u8,
6876
}
6977

78+
impl Serialize for TaggedBase64 {
79+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
80+
where
81+
S: Serializer,
82+
{
83+
if serializer.is_human_readable() {
84+
// If we are serializing to a human-readable format, be nice and just display the
85+
// tagged base 64 as a string.
86+
Serialize::serialize(&self.to_string(), serializer)
87+
} else {
88+
// For binary formats, convert to bytes (using CanonicalSerialize) and write the bytes.
89+
let mut bytes = vec![];
90+
CanonicalSerialize::serialize(self, &mut bytes).map_err(S::Error::custom)?;
91+
Serialize::serialize(&bytes, serializer)
92+
}
93+
}
94+
}
95+
96+
impl<'a> Deserialize<'a> for TaggedBase64 {
97+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
98+
where
99+
D: Deserializer<'a>,
100+
{
101+
if deserializer.is_human_readable() {
102+
// If we are deserializing a human-readable format, the serializer would have written
103+
// the tagged base 64 as a string, so deserialize a string and then parse it.
104+
Self::from_str(Deserialize::deserialize(deserializer)?).map_err(D::Error::custom)
105+
} else {
106+
// Otherwise, this is a binary format; deserialize bytes and then convert the bytes to
107+
// TaggedBase64 using CanonicalDeserialize.
108+
let bytes = <Vec<u8> as Deserialize>::deserialize(deserializer)?;
109+
CanonicalDeserialize::deserialize(bytes.as_slice()).map_err(D::Error::custom)
110+
}
111+
}
112+
}
113+
70114
/// JavaScript-compatible wrapper for TaggedBase64
71115
///
72116
/// The primary difference is that JsTaggedBase64 returns errors
@@ -77,7 +121,7 @@ pub struct JsTaggedBase64 {
77121
tb64: TaggedBase64,
78122
}
79123

80-
#[derive(Debug)]
124+
#[derive(Debug, Snafu)]
81125
pub enum Tb64Error {
82126
/// An invalid character was found in the tag.
83127
InvalidTag,
@@ -87,38 +131,25 @@ pub enum Tb64Error {
87131
MissingChecksum,
88132
/// An invalid byte was found while decoding the base64-encoded value.
89133
/// The offset and offending byte are provided.
90-
InvalidByte(usize, u8),
134+
#[snafu(display(
135+
"An invalid byte ({:#x}) was found at offset {} while decoding the base64-encoded value.",
136+
byte,
137+
offset
138+
))]
139+
InvalidByte { offset: usize, byte: u8 },
91140
/// The last non-padding input symbol's encoded 6 bits have
92141
/// nonzero bits that will be discarded. This is indicative of
93142
/// corrupted or truncated Base64. Unlike InvalidByte, which
94143
/// reports symbols that aren't in the alphabet, this error is for
95144
/// symbols that are in the alphabet but represent nonsensical
96145
/// encodings.
97-
InvalidLastSymbol(usize, u8),
146+
InvalidLastSymbol { offset: usize, byte: u8 },
98147
/// The length of the base64-encoded value is invalid.
99148
InvalidLength,
100149
/// The checksum was truncated or did not match.
101150
InvalidChecksum,
102-
}
103-
104-
impl fmt::Display for Tb64Error {
105-
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
106-
match *self {
107-
Tb64Error::InvalidTag =>
108-
write!(f, "An invalid character was found in the tag."),
109-
Tb64Error::MissingDelimiter =>
110-
write!(f, "Missing delimiter ({}).", TB64_DELIM),
111-
Tb64Error::MissingChecksum =>
112-
write!(f, "Missing checksum in value."),
113-
Tb64Error::InvalidByte(offset, byte) =>
114-
write!(f, "An invalid byte ({:#0x}) was found at offset {} while decoding the base64-encoded value. The offset and offending byte are provided.", byte, offset),
115-
Tb64Error::InvalidLastSymbol(offset, byte) => write!(f, "The last non-padding input symbol's encoded 6 bits have nonzero bits that will be discarded. This is indicative of corrupted or truncated Base64. Unlike InvalidByte, which reports symbols that aren't in the alphabet, this error is for symbols that are in the alphabet but represent nonsensical encodings. Invalid byte ({:#0x}) at offset {}.", byte, offset),
116-
Tb64Error::InvalidLength =>
117-
write!(f, "The length of the base64-encoded value is invalid."),
118-
Tb64Error::InvalidChecksum =>
119-
write!(f, "The checksum was truncated or did not match."),
120-
}
121-
}
151+
/// The data did not encode the expected type.
152+
InvalidData,
122153
}
123154

124155
/// Converts a TaggedBase64 value to a String.
@@ -296,15 +327,23 @@ impl TaggedBase64 {
296327
/// Wraps the underlying base64 decoder.
297328
pub fn decode_raw(value: &str) -> Result<Vec<u8>, Tb64Error> {
298329
base64::decode_config(value, TB64_CONFIG).map_err(|err| match err {
299-
base64::DecodeError::InvalidByte(offset, byte) => Tb64Error::InvalidByte(offset, byte),
330+
base64::DecodeError::InvalidByte(offset, byte) => {
331+
Tb64Error::InvalidByte { offset, byte }
332+
}
300333
base64::DecodeError::InvalidLength => Tb64Error::InvalidLength,
301334
base64::DecodeError::InvalidLastSymbol(offset, byte) => {
302-
Tb64Error::InvalidLastSymbol(offset, byte)
335+
Tb64Error::InvalidLastSymbol { offset, byte }
303336
}
304337
})
305338
}
306339
}
307340

341+
impl AsRef<[u8]> for TaggedBase64 {
342+
fn as_ref(&self) -> &[u8] {
343+
&self.value
344+
}
345+
}
346+
308347
/// Converts any object that supports the Display trait to a JsValue for
309348
/// passing to Javascript.
310349
///
@@ -375,3 +414,15 @@ impl JsTaggedBase64 {
375414
self.tb64.to_string()
376415
}
377416
}
417+
418+
/// Trait for types whose serialization is not human-readable.
419+
///
420+
/// Such types have a human-readable tag which is used to identify tagged base
421+
/// 64 blobs representing a serialization of that type.
422+
///
423+
/// Rather than implement this trait manually, it is recommended to use the
424+
/// [macro@tagged] macro to specify a tag for your type. That macro also
425+
/// derives appropriate serde implementations for serializing as an opaque blob.
426+
pub trait Tagged {
427+
fn tag() -> String;
428+
}

0 commit comments

Comments
 (0)