diff --git a/Cargo.toml b/Cargo.toml index 3ada0e7db..8aedfa64f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,4 @@ members = [ "primitives", "relation", "utilities", - "utilities_derive", ] diff --git a/plonk/Cargo.toml b/plonk/Cargo.toml index 1586c1db4..312457c1e 100644 --- a/plonk/Cargo.toml +++ b/plonk/Cargo.toml @@ -32,6 +32,7 @@ rand_chacha = { version = "0.3.1" } rayon = { version = "1.5.0", optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } sha3 = "^0.10" +tagged-base64 = { git = "https://github.com/espressosystems/tagged-base64", tag = "0.2.4" } [dev-dependencies] ark-ed-on-bls12-377 = { git = "https://github.com/arkworks-rs/curves", rev = "677b4ae751a274037880ede86e9b6f30f62635af" } diff --git a/plonk/src/proof_system/structs.rs b/plonk/src/proof_system/structs.rs index 8676ff365..564c98200 100644 --- a/plonk/src/proof_system/structs.rs +++ b/plonk/src/proof_system/structs.rs @@ -41,7 +41,8 @@ use jf_relation::{ }, PlonkCircuit, }; -use jf_utils::{field_switching, fq_to_fr, fr_to_fq, tagged_blob}; +use jf_utils::{field_switching, fq_to_fr, fr_to_fq}; +use tagged_base64::tagged; /// Universal StructuredReferenceString pub type UniversalSrs = UnivariateUniversalParams; @@ -51,7 +52,7 @@ pub type CommitKey = UnivariateProverParam<::G1Affine>; pub type OpenKey = UnivariateVerifierParam; /// A Plonk SNARK proof. -#[tagged_blob(tag::PROOF)] +#[tagged(tag::PROOF)] #[derive(Debug, Clone, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize, Derivative)] #[derivative(Hash(bound = "E:PairingEngine"))] pub struct Proof { @@ -226,7 +227,7 @@ pub struct PlookupProof { } /// An aggregated SNARK proof that batchly proving multiple instances. -#[tagged_blob(tag::BATCHPROOF)] +#[tagged(tag::BATCHPROOF)] #[derive(Debug, Clone, PartialEq, Eq, CanonicalSerialize, CanonicalDeserialize, Derivative)] #[derivative(Hash(bound = "E:PairingEngine"))] pub struct BatchProof { diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index afb45b5d8..ca7f63308 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -34,6 +34,7 @@ rand_chacha = { version = "0.3.1", default-features = false } rayon = { version = "1.5.0", optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } sha2 = { version = "0.10.1", default-features = false } +tagged-base64 = { git = "https://github.com/espressosystems/tagged-base64", tag = "0.2.4" } zeroize = { version = "1.3", default-features = false } [dev-dependencies] diff --git a/primitives/src/merkle_tree.rs b/primitives/src/merkle_tree.rs index c1cebc439..8940349a7 100644 --- a/primitives/src/merkle_tree.rs +++ b/primitives/src/merkle_tree.rs @@ -31,10 +31,14 @@ use ark_std::{ vec, vec::Vec, }; -use core::{convert::TryFrom, fmt::Debug}; +use core::{ + convert::{TryFrom, TryInto}, + fmt::Debug, +}; use espresso_systems_common::jellyfish::tag; -use jf_utils::tagged_blob; +use jf_utils::field_elem; use serde::{Deserialize, Serialize}; +use tagged_base64::tagged; #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Deserialize, Serialize)] /// Enum for identifying a position of a node (left, middle or right). @@ -192,7 +196,7 @@ impl MerklePath { } /// Represents the value for a node in the merkle tree. -#[tagged_blob(tag::NODE)] +#[tagged(tag::NODE)] #[derive( Clone, Debug, PartialEq, Eq, Hash, Default, CanonicalSerialize, CanonicalDeserialize, Copy, )] @@ -337,7 +341,7 @@ where Leaf { value: NodeValue, uid: u64, - #[serde(with = "jf_utils::field_elem")] + #[serde(with = "field_elem")] elem: F, }, } @@ -820,7 +824,7 @@ where } /// Data struct for a merkle leaf. -#[tagged_blob(tag::LEAF)] +#[tagged(tag::LEAF)] #[derive( Clone, Debug, PartialEq, Eq, Hash, Default, CanonicalSerialize, CanonicalDeserialize, Copy, )] diff --git a/primitives/src/signatures/schnorr.rs b/primitives/src/signatures/schnorr.rs index a9cd6022d..c726031f4 100644 --- a/primitives/src/signatures/schnorr.rs +++ b/primitives/src/signatures/schnorr.rs @@ -28,8 +28,10 @@ use ark_std::{ string::ToString, vec, }; +use core::convert::TryInto; use espresso_systems_common::jellyfish::tag; -use jf_utils::{fq_to_fr, fq_to_fr_with_mask, fr_to_fq, tagged_blob}; +use jf_utils::{fq_to_fr, fq_to_fr_with_mask, fr_to_fq}; +use tagged_base64::tagged; use zeroize::Zeroize; /// Schnorr signature scheme. @@ -125,7 +127,7 @@ impl SignKey { /// Signature public verification key // derive zeroize here so that keypair can be zeroized -#[tagged_blob(tag::SCHNORRVERKEY)] +#[tagged(tag::SCHNORRVERKEY)] #[derive(CanonicalSerialize, CanonicalDeserialize, Derivative)] #[derivative( Debug(bound = "P: Parameters"), @@ -194,7 +196,7 @@ impl VerKey

{ /// Signature secret key pair used to sign messages // make sure sk can be zeroized -#[tagged_blob(tag::SIGNKEYPAIR)] +#[tagged(tag::SIGNKEYPAIR)] #[derive(CanonicalSerialize, CanonicalDeserialize, Derivative)] #[derivative( Debug(bound = "P: Parameters"), @@ -215,7 +217,7 @@ where // ===================================================== /// The signature of Schnorr signature scheme -#[tagged_blob(tag::SIG)] +#[tagged(tag::SIG)] #[derive(CanonicalSerialize, CanonicalDeserialize, Derivative)] #[derivative( Debug(bound = "P: Parameters"), diff --git a/scripts/check_no_std.sh b/scripts/check_no_std.sh index b51398774..d77016684 100755 --- a/scripts/check_no_std.sh +++ b/scripts/check_no_std.sh @@ -2,7 +2,6 @@ set -x -cargo-nono check --no-default-features --package jf-utils-derive cargo-nono check --no-default-features --package jf-utils cargo-nono check --no-default-features --package jf-rescue cargo-nono check --no-default-features --package jf-primitives diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index 234ded99e..32885e1f5 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -8,5 +8,4 @@ cargo test --release -p jf-plonk -- -Zunstable-options --report-time cargo test --release -p jf-primitives -- -Zunstable-options --report-time cargo test --release -p jf-rescue -- -Zunstable-options --report-time cargo test --release -p jf-utils -- -Zunstable-options --report-time -cargo test --release -p jf-utils-derive -- -Zunstable-options --report-time diff --git a/utilities/Cargo.toml b/utilities/Cargo.toml index 2afa9c5f7..b8a19816a 100644 --- a/utilities/Cargo.toml +++ b/utilities/Cargo.toml @@ -13,12 +13,10 @@ ark-ff = { version = "0.3.0", default-features = false, features = [ "asm" ] } ark-serialize = { version = "0.3.0", default-features = false } ark-std = { version = "0.3.0", default-features = false } digest = { version = "0.10.1", default-features = false } -displaydoc = { version = "0.2.3", default-features = false } -jf-utils-derive = { path = "../utilities_derive" } rayon = { version = "1.5.0", optional = true } serde = { version = "1.0", default-features = false, features = ["derive"] } sha2 = { version = "0.10.1", default-features = false } -tagged-base64 = { git = "https://github.com/EspressoSystems/tagged-base64", tag = "0.2.0" } +tagged-base64 = { git = "https://github.com/espressosystems/tagged-base64", tag = "0.2.4" } [dev-dependencies] ark-bls12-377 = { git = "https://github.com/arkworks-rs/curves", rev = "677b4ae751a274037880ede86e9b6f30f62635af" } @@ -28,8 +26,6 @@ ark-ed-on-bls12-377 = { git = "https://github.com/arkworks-rs/curves", rev = "67 ark-ed-on-bls12-381 = "0.3.0" ark-ed-on-bls12-381-bandersnatch = { git = "https://github.com/arkworks-rs/curves", rev = "677b4ae751a274037880ede86e9b6f30f62635af" } ark-ed-on-bn254 = "0.3.0" -ark-serialize = { version = "0.3.0", features = ["derive"] } -serde_json = "1.0" [features] default = [] diff --git a/utilities/src/serialize.rs b/utilities/src/serialize.rs index f1ca2857c..e7d43d36c 100644 --- a/utilities/src/serialize.rs +++ b/utilities/src/serialize.rs @@ -6,11 +6,8 @@ //! Various serialization functions. -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; -use ark_std::{marker::PhantomData, string::String, vec::Vec}; -use displaydoc::Display; +use ark_std::vec::Vec; use serde::{Deserialize, Serialize}; -use tagged_base64::{TaggedBase64, Tb64Error}; /// A helper for converting CanonicalSerde bytes to standard Serde bytes. /// use this struct as intermediate target instead of directly deriving @@ -44,196 +41,6 @@ macro_rules! deserialize_canonical_bytes { }; } -/// Trait for types whose serialization is not human-readable. -/// -/// Such types have a human-readable tag which is used to identify tagged base -/// 64 blobs representing a serialization of that type. -/// -/// Rather than implement this trait manually, it is recommended to use the -/// [macro@tagged_blob] macro to specify a tag for your type. That macro also -/// derives appropriate serde implementations for serializing as an opaque blob. -pub trait Tagged { - fn tag() -> String; -} - -/// Helper type for serializing tagged blobs. -/// -/// A type which can only be serialized using ark_serialize (for example, -/// cryptographic primitives) can derive serde implementations using `serde(from -/// = "TaggedBlob", into = "TaggedBlob")`. The serde implementations for -/// TaggedBlob generate either a packed bytes encoding, if the serialization -/// format is binary, or a tagged base 64 encoding, if the serialization format -/// is human-readable. -/// -/// Types which are serialized using TaggedBlob can then be embedded as fields -/// in structs which derive serde implementations, and those structs can then be -/// serialized using an efficient binary encoding or a browser-friendly, -/// human-readable encoding like JSON. -/// -/// Rather than manually tag types with `serde(from = "TaggedBlob", into = -/// "TaggedBlob")`, it is recommended to use the [macro@tagged_blob] macro, -/// which will automatically add the appropriate serde attributes as well as the -/// necessary [Tagged] and [From] implementations to allow `serde(from)` to -/// work. -#[derive(Deserialize, Serialize)] -#[serde(transparent)] -pub struct TaggedBlob { - #[serde(with = "tagged_blob")] - inner: (CanonicalBytes, PhantomData), -} - -impl TaggedBlob { - pub fn bytes(&self) -> &CanonicalBytes { - &self.inner.0 - } - - pub fn tagged_base64(&self) -> Result { - TaggedBase64::new(T::tag().as_str(), &self.inner.0 .0) - } -} - -impl From for TaggedBlob { - fn from(v: T) -> Self { - Self { - inner: (CanonicalBytes::from(v), Default::default()), - } - } -} - -impl From<&T> for TaggedBlob { - fn from(v: &T) -> Self { - let mut bytes = Vec::new(); - v.serialize(&mut bytes) - .expect("fail to serialize to canonical bytes"); - Self { - inner: (CanonicalBytes(bytes), Default::default()), - } - } -} - -#[derive(Debug, Display)] -pub enum TaggedBlobError { - /// TaggedBase64 parsing failure - Base64Error { source: Tb64Error }, - /// CanonicalSerialize failure - DeserializationError { - source: ark_serialize::SerializationError, - }, -} - -impl ark_std::str::FromStr for TaggedBlob { - type Err = TaggedBlobError; - - fn from_str(s: &str) -> Result { - let tb64 = - TaggedBase64::parse(s).map_err(|source| TaggedBlobError::Base64Error { source })?; - if tb64.tag() == T::tag() { - Ok(Self { - inner: (CanonicalBytes(tb64.value()), Default::default()), - }) - } else { - Err(TaggedBlobError::Base64Error { - source: Tb64Error::InvalidTag, - }) - } - } -} - -pub mod tagged_blob { - use super::*; - use ark_std::format; - use serde::{ - de::{Deserializer, Error as DeError}, - ser::{Error as SerError, Serializer}, - }; - - pub fn serialize_with_tag( - tag: &str, - bytes: &CanonicalBytes, - serializer: S, - ) -> Result { - if serializer.is_human_readable() { - let string = tagged_base64::to_string( - &TaggedBase64::new(tag, &bytes.0) - .map_err(|err| S::Error::custom(format!("{}", err)))?, - ); - Serialize::serialize(&string, serializer) - } else { - Serialize::serialize(&bytes.0, serializer) - } - } - - pub fn serialize( - v: &(CanonicalBytes, PhantomData), - serializer: S, - ) -> Result { - serialize_with_tag(T::tag().as_str(), &v.0, serializer) - } - - pub fn deserialize_with_tag<'de, D: Deserializer<'de>>( - tag: &str, - deserializer: D, - ) -> Result { - let bytes = if deserializer.is_human_readable() { - let string = ::deserialize(deserializer)?; - let tb64 = - TaggedBase64::parse(&string).map_err(|err| D::Error::custom(format!("{}", err)))?; - if tb64.tag() == tag { - tb64.value() - } else { - return Err(D::Error::custom(format!( - "tag mismatch: expected {}, but got {}", - tag, - tb64.tag() - ))); - } - } else { - as Deserialize>::deserialize(deserializer)? - }; - Ok(CanonicalBytes(bytes)) - } - - pub fn deserialize<'de, D: Deserializer<'de>, T: Tagged>( - deserializer: D, - ) -> Result<(CanonicalBytes, PhantomData), D::Error> { - let bytes = deserialize_with_tag(T::tag().as_str(), deserializer)?; - Ok((bytes, Default::default())) - } - - #[cfg(test)] - mod test { - use crate as jf_utils; - use ark_serialize::*; - use jf_utils::tagged_blob; - use std::vec::Vec; - - #[tagged_blob("A")] - #[derive(Debug, Clone, PartialEq, Eq, CanonicalDeserialize, CanonicalSerialize)] - pub struct A(Vec); - - #[test] - fn test_tagged_blob_static_str() { - let a = A(Vec::new()); - let str = serde_json::to_string(&a).unwrap(); - assert_eq!(str, r#""A~AAAAAAAAAABr""#); - } - - mod tags { - pub const B: &str = "B"; - } - #[tagged_blob(tags::B)] - #[derive(Debug, Clone, PartialEq, Eq, CanonicalDeserialize, CanonicalSerialize)] - pub struct B(Vec); - - #[test] - fn test_tagged_blob_const_str() { - let b = B(Vec::new()); - let str = serde_json::to_string(&b).unwrap(); - assert_eq!(str, r#""B~AAAAAAAAAADg""#); - } - } -} - /// Serializers for finite field elements. /// /// Field elements are typically foreign types that we cannot apply the @@ -242,11 +49,13 @@ pub mod tagged_blob { /// definition. pub mod field_elem { use super::*; + use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use ark_std::format; use serde::{ de::{Deserializer, Error as DeError}, ser::{Error as SerError, Serializer}, }; + use tagged_base64::TaggedBase64; pub fn serialize( elem: &T, @@ -254,20 +63,24 @@ pub mod field_elem { ) -> Result { let mut bytes = Vec::new(); T::serialize(elem, &mut bytes).map_err(|err| S::Error::custom(format!("{}", err)))?; - tagged_blob::serialize_with_tag("FIELD", &CanonicalBytes(bytes), serializer) + Serialize::serialize(&TaggedBase64::new("FIELD", &bytes).unwrap(), serializer) } pub fn deserialize<'de, D: Deserializer<'de>, T: CanonicalDeserialize>( deserializer: D, ) -> Result { - let bytes = tagged_blob::deserialize_with_tag("FIELD", deserializer)?; - T::deserialize(&*bytes.0).map_err(|err| D::Error::custom(format!("{}", err))) + let tb64 = ::deserialize(deserializer)?; + if tb64.tag() == "FIELD" { + T::deserialize(tb64.as_ref()).map_err(|err| D::Error::custom(format!("{}", err))) + } else { + Err(D::Error::custom(format!( + "incorrect tag (expected FIELD, got {})", + tb64.tag() + ))) + } } } -extern crate jf_utils_derive; -pub use jf_utils_derive::*; - #[macro_export] macro_rules! test_serde_default { ($struct:tt) => { diff --git a/utilities_derive/Cargo.toml b/utilities_derive/Cargo.toml deleted file mode 100644 index cfc51f4f3..000000000 --- a/utilities_derive/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "jf-utils-derive" -description = "Procedural macros for deriving serialization code for Jellyfish types" -version = "0.1.2" -authors = ["Espresso Systems "] -edition = "2018" -license = "MIT" - -[lib] -proc-macro = true - -[dependencies] -ark-std = { version = "0.3.0", default-features = false } -quote = "1.0" -syn = "1.0" - -[dev-dependencies] -ark-bls12-381 = { version = "0.3.0", default-features = false, features = ["curve"] } -ark-serialize = { version = "0.3.0", default-features = false, features = ["derive"] } -bincode = { version = "1.3.3", default-features = false } -jf-utils = { path = "../utilities" } -rand_chacha = { version = "0.3.1" } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0.61" diff --git a/utilities_derive/src/lib.rs b/utilities_derive/src/lib.rs deleted file mode 100644 index 585bf4f76..000000000 --- a/utilities_derive/src/lib.rs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) 2022 Espresso Systems (espressosys.com) -// This file is part of the Jellyfish library. - -// You should have received a copy of the MIT License -// along with the Jellyfish library. If not, see . - -#![no_std] - -extern crate proc_macro; - -use ark_std::format; -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, AttributeArgs, Item, Meta, NestedMeta}; - -/// Derive serdes for a type which serializes as a binary blob. -/// -/// This macro can be used to easily derive friendly serde implementations for a -/// binary type which implements -/// [CanonicalSerialize](ark_serialize::CanonicalSerialize) and -/// [CanonicalDeserialize](ark_serialize::CanonicalDeserialize). This is useful -/// for cryptographic primitives and other types which do not have a -/// human-readable serialization, but which may be embedded in structs with a -/// human-readable serialization. The serde implementations derived by this -/// macro will serialize the type as bytes for binary encodings and as base 64 -/// for human readable encodings. -/// -/// Specifically, this macro does 4 things when applied to a type definition: -/// * It adds `#[derive(Serialize, Deserialize)]` to the type definition, along -/// with serde attributes to serialize using the -/// [TaggedBlob](../jf_utils/struct.TaggedBlob.html) serialization helper. -/// * It creates an implementation of [Tagged](../jf_utils/trait.Tagged.html) -/// for the type using the specified tag. This tag will be used to identify -/// base 64 strings which represent this type in human-readable encodings. -/// * It creates an implementation of `TryFrom>` for the type `T`, -/// which is needed to make the `serde(try_from)` attribute work. -/// * It creates implementations of [Display](ark_std::fmt::Display) and -/// [FromStr](ark_std::str::FromStr) using tagged base 64 as a display format. -/// This allows tagged blob types to be conveniently displayed and read to and -/// from user interfaces in a manner consistent with how they are serialized. -/// -/// Usage example: -/// -/// ``` -/// #[macro_use] extern crate jf_utils_derive; -/// use ark_serialize::*; -/// -/// #[tagged_blob("NUL")] -/// #[derive(Clone, CanonicalSerialize, CanonicalDeserialize, /* any other derives */)] -/// pub struct Nullifier( -/// // This type can only be serialied as an opaque, binary blob using ark_serialize. -/// pub(crate) ark_bls12_381::Fr, -/// ); -/// ``` -/// -/// The type Nullifier can now be serialized as binary: -/// ``` -/// # use ark_serialize::*; -/// # use ark_std::UniformRand; -/// # use jf_utils_derive::tagged_blob; -/// # use rand_chacha::{ChaChaRng, rand_core::SeedableRng}; -/// # #[tagged_blob("NUL")] -/// # #[derive(Clone, CanonicalSerialize, CanonicalDeserialize, /* any other derives */)] -/// # struct Nullifier(ark_bls12_381::Fr); -/// # let nullifier = Nullifier(ark_bls12_381::Fr::rand(&mut ChaChaRng::from_seed([42; 32]))); -/// bincode::serialize(&nullifier).unwrap(); -/// ``` -/// or as base64: -/// ``` -/// # use ark_serialize::*; -/// # use ark_std::UniformRand; -/// # use jf_utils_derive::tagged_blob; -/// # use rand_chacha::{ChaChaRng, rand_core::SeedableRng}; -/// # #[tagged_blob("NUL")] -/// # #[derive(Clone, CanonicalSerialize, CanonicalDeserialize, /* any other derives */)] -/// # struct Nullifier(ark_bls12_381::Fr); -/// # let nullifier = Nullifier(ark_bls12_381::Fr::rand(&mut ChaChaRng::from_seed([42; 32]))); -/// serde_json::to_string(&nullifier).unwrap(); -/// ``` -/// which will produce a tagged base64 string like -/// "NUL~8oaujwbov8h4eEq7HFpqW6mIXhVbtJGxLUgiKrGpMCoJ". -#[proc_macro_attribute] -pub fn tagged_blob(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as AttributeArgs); - let input = parse_macro_input!(input as Item); - let (name, generics) = match &input { - Item::Struct(item) => (&item.ident, &item.generics), - Item::Enum(item) => (&item.ident, &item.generics), - _ => panic!("expected struct or enum"), - }; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let tag: &dyn quote::ToTokens = match args.as_slice() { - [NestedMeta::Lit(tag)] => tag, - [NestedMeta::Meta(Meta::Path(path))] => path, - x => panic!("tagged_blob takes one argument, the tag, as a string literal or expression, found {:?}", x), - }; - let serde_str = format!("jf_utils::TaggedBlob<{}>", quote!(#name #ty_generics)); - let output = quote! { - #[derive(serde::Serialize, serde::Deserialize)] - #[serde(try_from = #serde_str, into = #serde_str)] - // Override the inferred bound for Serialize/Deserialize impls. If we're converting to and - // from CanonicalBytes as an intermediate, the impls should work for any generic parameters. - #[serde(bound = "")] - #input - - impl #impl_generics jf_utils::Tagged for #name #ty_generics #where_clause { - fn tag() -> ark_std::string::String { - ark_std::string::String::from(#tag) - } - } - - impl #impl_generics core::convert::TryFrom> - for #name #ty_generics - #where_clause - { - type Error = ark_serialize::SerializationError; - fn try_from(v: jf_utils::TaggedBlob) -> Result { - ::deserialize( - &*v.bytes().0, - ) - } - } - - impl #impl_generics ark_std::fmt::Display for #name #ty_generics #where_clause { - fn fmt(&self, f: &mut ark_std::fmt::Formatter<'_>) -> ark_std::fmt::Result { - ark_std::write!( - f, "{}", - jf_utils::TaggedBlob::from(self) - .tagged_base64() - .map_err(|_| ark_std::fmt::Error)? - ) - } - } - - impl #impl_generics ark_std::str::FromStr for #name #ty_generics #where_clause { - type Err = jf_utils::TaggedBlobError; - fn from_str(s: &str) -> Result { - use core::convert::TryFrom; - Self::try_from(jf_utils::TaggedBlob::from_str(s)?) - .map_err(|source| { - jf_utils::TaggedBlobError::DeserializationError { source } - }) - } - } - }; - output.into() -}