InvoiceRequest metadata and payer id derivation
authorJeffrey Czyz <jkczyz@gmail.com>
Mon, 30 Jan 2023 20:56:42 +0000 (14:56 -0600)
committerJeffrey Czyz <jkczyz@gmail.com>
Thu, 20 Apr 2023 02:31:06 +0000 (21:31 -0500)
Add support for deriving a transient payer id for each InvoiceRequest
from an ExpandedKey and a nonce. This facilitates payer privacy by not
tying any InvoiceRequest to any other nor to the payer's node id.

Additionally, support stateless Invoice verification by setting payer
metadata using an HMAC over the nonce and the remaining TLV records,
which will be later verified when receiving an Invoice response.

lightning/src/offers/invoice_request.rs
lightning/src/offers/offer.rs
lightning/src/offers/payer.rs
lightning/src/offers/refund.rs
lightning/src/offers/signer.rs

index 124ecc95c7a71aefdb4e61010f3a8ef790f32478..26151dfbd18500bf2392c89a2b900c945d9896a6 100644 (file)
 
 use bitcoin::blockdata::constants::ChainHash;
 use bitcoin::network::constants::Network;
-use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, self};
+use bitcoin::secp256k1::{KeyPair, Message, PublicKey, Secp256k1, self};
 use bitcoin::secp256k1::schnorr::Signature;
-use core::convert::TryFrom;
+use core::convert::{Infallible, TryFrom};
+use core::ops::Deref;
+use crate::chain::keysinterface::EntropySource;
 use crate::io;
 use crate::ln::PaymentHash;
 use crate::ln::features::InvoiceRequestFeatures;
-use crate::ln::inbound_payment::ExpandedKey;
+use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
 use crate::ln::msgs::DecodeError;
 use crate::offers::invoice::{BlindedPayInfo, InvoiceBuilder};
 use crate::offers::merkle::{SignError, SignatureTlvStream, SignatureTlvStreamRef, TlvStream, self};
 use crate::offers::offer::{Offer, OfferContents, OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
 use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
+use crate::offers::signer::{Metadata, MetadataMaterial};
 use crate::onion_message::BlindedPath;
 use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
@@ -75,28 +78,83 @@ use crate::prelude::*;
 
 const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice_request", "signature");
 
+const IV_BYTES: &[u8; IV_LEN] = b"LDK Invreq ~~~~~";
+
 /// Builds an [`InvoiceRequest`] from an [`Offer`] for the "offer to be paid" flow.
 ///
 /// See [module-level documentation] for usage.
 ///
 /// [module-level documentation]: self
-pub struct InvoiceRequestBuilder<'a> {
+pub struct InvoiceRequestBuilder<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> {
        offer: &'a Offer,
-       invoice_request: InvoiceRequestContents,
+       invoice_request: InvoiceRequestContentsWithoutPayerId,
+       payer_id: Option<PublicKey>,
+       payer_id_strategy: core::marker::PhantomData<P>,
+       secp_ctx: Option<&'b Secp256k1<T>>,
 }
 
-impl<'a> InvoiceRequestBuilder<'a> {
+/// Indicates how [`InvoiceRequest::payer_id`] will be set.
+pub trait PayerIdStrategy {}
+
+/// [`InvoiceRequest::payer_id`] will be explicitly set.
+pub struct ExplicitPayerId {}
+
+/// [`InvoiceRequest::payer_id`] will be derived.
+pub struct DerivedPayerId {}
+
+impl PayerIdStrategy for ExplicitPayerId {}
+impl PayerIdStrategy for DerivedPayerId {}
+
+impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerId, T> {
        pub(super) fn new(offer: &'a Offer, metadata: Vec<u8>, payer_id: PublicKey) -> Self {
                Self {
                        offer,
-                       invoice_request: InvoiceRequestContents {
-                               inner: InvoiceRequestContentsWithoutPayerId {
-                                       payer: PayerContents(metadata), offer: offer.contents.clone(), chain: None,
-                                       amount_msats: None, features: InvoiceRequestFeatures::empty(), quantity: None,
-                                       payer_note: None,
-                               },
-                               payer_id,
-                       },
+                       invoice_request: Self::create_contents(offer, Metadata::Bytes(metadata)),
+                       payer_id: Some(payer_id),
+                       payer_id_strategy: core::marker::PhantomData,
+                       secp_ctx: None,
+               }
+       }
+
+       pub(super) fn deriving_metadata<ES: Deref>(
+               offer: &'a Offer, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
+       ) -> Self where ES::Target: EntropySource {
+               let nonce = Nonce::from_entropy_source(entropy_source);
+               let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
+               let metadata = Metadata::Derived(derivation_material);
+               Self {
+                       offer,
+                       invoice_request: Self::create_contents(offer, metadata),
+                       payer_id: Some(payer_id),
+                       payer_id_strategy: core::marker::PhantomData,
+                       secp_ctx: None,
+               }
+       }
+}
+
+impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> {
+       pub(super) fn deriving_payer_id<ES: Deref>(
+               offer: &'a Offer, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
+       ) -> Self where ES::Target: EntropySource {
+               let nonce = Nonce::from_entropy_source(entropy_source);
+               let derivation_material = MetadataMaterial::new(nonce, expanded_key, IV_BYTES);
+               let metadata = Metadata::DerivedSigningPubkey(derivation_material);
+               Self {
+                       offer,
+                       invoice_request: Self::create_contents(offer, metadata),
+                       payer_id: None,
+                       payer_id_strategy: core::marker::PhantomData,
+                       secp_ctx: Some(secp_ctx),
+               }
+       }
+}
+
+impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, P, T> {
+       fn create_contents(offer: &Offer, metadata: Metadata) -> InvoiceRequestContentsWithoutPayerId {
+               let offer = offer.contents.clone();
+               InvoiceRequestContentsWithoutPayerId {
+                       payer: PayerContents(metadata), offer, chain: None, amount_msats: None,
+                       features: InvoiceRequestFeatures::empty(), quantity: None, payer_note: None,
                }
        }
 
@@ -111,7 +169,7 @@ impl<'a> InvoiceRequestBuilder<'a> {
                        return Err(SemanticError::UnsupportedChain);
                }
 
-               self.invoice_request.inner.chain = Some(chain);
+               self.invoice_request.chain = Some(chain);
                Ok(self)
        }
 
@@ -122,10 +180,10 @@ impl<'a> InvoiceRequestBuilder<'a> {
        ///
        /// [`quantity`]: Self::quantity
        pub fn amount_msats(mut self, amount_msats: u64) -> Result<Self, SemanticError> {
-               self.invoice_request.inner.offer.check_amount_msats_for_quantity(
-                       Some(amount_msats), self.invoice_request.inner.quantity
+               self.invoice_request.offer.check_amount_msats_for_quantity(
+                       Some(amount_msats), self.invoice_request.quantity
                )?;
-               self.invoice_request.inner.amount_msats = Some(amount_msats);
+               self.invoice_request.amount_msats = Some(amount_msats);
                Ok(self)
        }
 
@@ -134,8 +192,8 @@ impl<'a> InvoiceRequestBuilder<'a> {
        ///
        /// Successive calls to this method will override the previous setting.
        pub fn quantity(mut self, quantity: u64) -> Result<Self, SemanticError> {
-               self.invoice_request.inner.offer.check_quantity(Some(quantity))?;
-               self.invoice_request.inner.quantity = Some(quantity);
+               self.invoice_request.offer.check_quantity(Some(quantity))?;
+               self.invoice_request.quantity = Some(quantity);
                Ok(self)
        }
 
@@ -143,13 +201,14 @@ impl<'a> InvoiceRequestBuilder<'a> {
        ///
        /// Successive calls to this method will override the previous setting.
        pub fn payer_note(mut self, payer_note: String) -> Self {
-               self.invoice_request.inner.payer_note = Some(payer_note);
+               self.invoice_request.payer_note = Some(payer_note);
                self
        }
 
-       /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed
-       /// by [`UnsignedInvoiceRequest::sign`].
-       pub fn build(mut self) -> Result<UnsignedInvoiceRequest<'a>, SemanticError> {
+       fn build_with_checks(mut self) -> Result<
+               (UnsignedInvoiceRequest<'a>, Option<KeyPair>, Option<&'b Secp256k1<T>>),
+               SemanticError
+       > {
                #[cfg(feature = "std")] {
                        if self.offer.is_expired() {
                                return Err(SemanticError::AlreadyExpired);
@@ -162,49 +221,114 @@ impl<'a> InvoiceRequestBuilder<'a> {
                }
 
                if chain == self.offer.implied_chain() {
-                       self.invoice_request.inner.chain = None;
+                       self.invoice_request.chain = None;
                }
 
-               if self.offer.amount().is_none() && self.invoice_request.inner.amount_msats.is_none() {
+               if self.offer.amount().is_none() && self.invoice_request.amount_msats.is_none() {
                        return Err(SemanticError::MissingAmount);
                }
 
-               self.invoice_request.inner.offer.check_quantity(self.invoice_request.inner.quantity)?;
-               self.invoice_request.inner.offer.check_amount_msats_for_quantity(
-                       self.invoice_request.inner.amount_msats, self.invoice_request.inner.quantity
+               self.invoice_request.offer.check_quantity(self.invoice_request.quantity)?;
+               self.invoice_request.offer.check_amount_msats_for_quantity(
+                       self.invoice_request.amount_msats, self.invoice_request.quantity
                )?;
 
-               let InvoiceRequestBuilder { offer, invoice_request } = self;
-               Ok(UnsignedInvoiceRequest { offer, invoice_request })
+               Ok(self.build_without_checks())
+       }
+
+       fn build_without_checks(mut self) ->
+               (UnsignedInvoiceRequest<'a>, Option<KeyPair>, Option<&'b Secp256k1<T>>)
+       {
+               // Create the metadata for stateless verification of an Invoice.
+               let mut keys = None;
+               let secp_ctx = self.secp_ctx.clone();
+               if self.invoice_request.payer.0.has_derivation_material() {
+                       let mut metadata = core::mem::take(&mut self.invoice_request.payer.0);
+
+                       let mut tlv_stream = self.invoice_request.as_tlv_stream();
+                       debug_assert!(tlv_stream.2.payer_id.is_none());
+                       tlv_stream.0.metadata = None;
+                       if !metadata.derives_keys() {
+                               tlv_stream.2.payer_id = self.payer_id.as_ref();
+                       }
+
+                       let (derived_metadata, derived_keys) = metadata.derive_from(tlv_stream, self.secp_ctx);
+                       metadata = derived_metadata;
+                       keys = derived_keys;
+                       if let Some(keys) = keys {
+                               debug_assert!(self.payer_id.is_none());
+                               self.payer_id = Some(keys.public_key());
+                       }
+
+                       self.invoice_request.payer.0 = metadata;
+               }
+
+               debug_assert!(self.invoice_request.payer.0.as_bytes().is_some());
+               debug_assert!(self.payer_id.is_some());
+               let payer_id = self.payer_id.unwrap();
+
+               let unsigned_invoice = UnsignedInvoiceRequest {
+                       offer: self.offer,
+                       invoice_request: InvoiceRequestContents {
+                               inner: self.invoice_request,
+                               payer_id,
+                       },
+               };
+
+               (unsigned_invoice, keys, secp_ctx)
+       }
+}
+
+impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, ExplicitPayerId, T> {
+       /// Builds an unsigned [`InvoiceRequest`] after checking for valid semantics. It can be signed
+       /// by [`UnsignedInvoiceRequest::sign`].
+       pub fn build(self) -> Result<UnsignedInvoiceRequest<'a>, SemanticError> {
+               let (unsigned_invoice_request, keys, _) = self.build_with_checks()?;
+               debug_assert!(keys.is_none());
+               Ok(unsigned_invoice_request)
+       }
+}
+
+impl<'a, 'b, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T> {
+       /// Builds a signed [`InvoiceRequest`] after checking for valid semantics.
+       pub fn build_and_sign(self) -> Result<InvoiceRequest, SemanticError> {
+               let (unsigned_invoice_request, keys, secp_ctx) = self.build_with_checks()?;
+               debug_assert!(keys.is_some());
+
+               let secp_ctx = secp_ctx.unwrap();
+               let keys = keys.unwrap();
+               let invoice_request = unsigned_invoice_request
+                       .sign::<_, Infallible>(|digest| Ok(secp_ctx.sign_schnorr_no_aux_rand(digest, &keys)))
+                       .unwrap();
+               Ok(invoice_request)
        }
 }
 
 #[cfg(test)]
-impl<'a> InvoiceRequestBuilder<'a> {
+impl<'a, 'b, P: PayerIdStrategy, T: secp256k1::Signing> InvoiceRequestBuilder<'a, 'b, P, T> {
        fn chain_unchecked(mut self, network: Network) -> Self {
                let chain = ChainHash::using_genesis_block(network);
-               self.invoice_request.inner.chain = Some(chain);
+               self.invoice_request.chain = Some(chain);
                self
        }
 
        fn amount_msats_unchecked(mut self, amount_msats: u64) -> Self {
-               self.invoice_request.inner.amount_msats = Some(amount_msats);
+               self.invoice_request.amount_msats = Some(amount_msats);
                self
        }
 
        fn features_unchecked(mut self, features: InvoiceRequestFeatures) -> Self {
-               self.invoice_request.inner.features = features;
+               self.invoice_request.features = features;
                self
        }
 
        fn quantity_unchecked(mut self, quantity: u64) -> Self {
-               self.invoice_request.inner.quantity = Some(quantity);
+               self.invoice_request.quantity = Some(quantity);
                self
        }
 
        pub(super) fn build_unchecked(self) -> UnsignedInvoiceRequest<'a> {
-               let InvoiceRequestBuilder { offer, invoice_request } = self;
-               UnsignedInvoiceRequest { offer, invoice_request }
+               self.build_without_checks().0
        }
 }
 
@@ -290,7 +414,7 @@ impl InvoiceRequest {
        ///
        /// [`payer_id`]: Self::payer_id
        pub fn metadata(&self) -> &[u8] {
-               &self.contents.inner.payer.0[..]
+               self.contents.metadata()
        }
 
        /// A chain from [`Offer::chains`] that the offer is valid for.
@@ -402,6 +526,10 @@ impl InvoiceRequest {
 }
 
 impl InvoiceRequestContents {
+       pub fn metadata(&self) -> &[u8] {
+               self.inner.metadata()
+       }
+
        pub(super) fn chain(&self) -> ChainHash {
                self.inner.chain()
        }
@@ -414,13 +542,17 @@ impl InvoiceRequestContents {
 }
 
 impl InvoiceRequestContentsWithoutPayerId {
+       pub(super) fn metadata(&self) -> &[u8] {
+               self.payer.0.as_bytes().map(|bytes| bytes.as_slice()).unwrap_or(&[])
+       }
+
        pub(super) fn chain(&self) -> ChainHash {
                self.chain.unwrap_or_else(|| self.offer.implied_chain())
        }
 
        pub(super) fn as_tlv_stream(&self) -> PartialInvoiceRequestTlvStreamRef {
                let payer = PayerTlvStreamRef {
-                       metadata: Some(&self.payer.0),
+                       metadata: self.payer.0.as_bytes(),
                };
 
                let offer = self.offer.as_tlv_stream();
@@ -530,7 +662,7 @@ impl TryFrom<PartialInvoiceRequestTlvStream> for InvoiceRequestContents {
 
                let payer = match metadata {
                        None => return Err(SemanticError::MissingPayerMetadata),
-                       Some(metadata) => PayerContents(metadata),
+                       Some(metadata) => PayerContents(Metadata::Bytes(metadata)),
                };
                let offer = OfferContents::try_from(offer_tlv_stream)?;
 
@@ -1038,7 +1170,7 @@ mod tests {
                let invoice_request = OfferBuilder::new("foo".into(), recipient_pubkey())
                        .amount_msats(1000)
                        .build().unwrap()
-                       .request_invoice(vec![42; 32], payer_pubkey()).unwrap()
+                       .request_invoice(vec![1; 32], payer_pubkey()).unwrap()
                        .build().unwrap()
                        .sign(payer_sign).unwrap();
 
index 6a8f956ae635eb8f3278ee85f48893a9fb8f5aaf..468018d55eccb1cf121dfec0bf01838d0978f444 100644 (file)
@@ -79,7 +79,7 @@ use crate::io;
 use crate::ln::features::OfferFeatures;
 use crate::ln::inbound_payment::{ExpandedKey, IV_LEN, Nonce};
 use crate::ln::msgs::MAX_VALUE_MSAT;
-use crate::offers::invoice_request::InvoiceRequestBuilder;
+use crate::offers::invoice_request::{DerivedPayerId, ExplicitPayerId, InvoiceRequestBuilder};
 use crate::offers::merkle::TlvStream;
 use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
 use crate::offers::signer::{Metadata, MetadataMaterial, self};
@@ -439,6 +439,51 @@ impl Offer {
                self.contents.signing_pubkey()
        }
 
+       /// Similar to [`Offer::request_invoice`] except it:
+       /// - derives the [`InvoiceRequest::payer_id`] such that a different key can be used for each
+       ///   request, and
+       /// - sets the [`InvoiceRequest::metadata`] when [`InvoiceRequestBuilder::build`] is called such
+       ///   that it can be used to determine if the invoice was requested using a base [`ExpandedKey`]
+       ///   from which the payer id was derived.
+       ///
+       /// Useful to protect the sender's privacy.
+       ///
+       /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
+       /// [`InvoiceRequest::metadata`]: crate::offers::invoice_request::InvoiceRequest::metadata
+       /// [`Invoice::verify`]: crate::offers::invoice::Invoice::verify
+       /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey
+       pub fn request_invoice_deriving_payer_id<'a, 'b, ES: Deref, T: secp256k1::Signing>(
+               &'a self, expanded_key: &ExpandedKey, entropy_source: ES, secp_ctx: &'b Secp256k1<T>
+       ) -> Result<InvoiceRequestBuilder<'a, 'b, DerivedPayerId, T>, SemanticError>
+       where
+               ES::Target: EntropySource,
+       {
+               if self.features().requires_unknown_bits() {
+                       return Err(SemanticError::UnknownRequiredFeatures);
+               }
+
+               Ok(InvoiceRequestBuilder::deriving_payer_id(self, expanded_key, entropy_source, secp_ctx))
+       }
+
+       /// Similar to [`Offer::request_invoice_deriving_payer_id`] except uses `payer_id` for the
+       /// [`InvoiceRequest::payer_id`] instead of deriving a different key for each request.
+       ///
+       /// Useful for recurring payments using the same `payer_id` with different invoices.
+       ///
+       /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
+       pub fn request_invoice_deriving_metadata<ES: Deref>(
+               &self, payer_id: PublicKey, expanded_key: &ExpandedKey, entropy_source: ES
+       ) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, SemanticError>
+       where
+               ES::Target: EntropySource,
+       {
+               if self.features().requires_unknown_bits() {
+                       return Err(SemanticError::UnknownRequiredFeatures);
+               }
+
+               Ok(InvoiceRequestBuilder::deriving_metadata(self, payer_id, expanded_key, entropy_source))
+       }
+
        /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
        /// will be reflected in the `Invoice` response.
        ///
@@ -454,7 +499,7 @@ impl Offer {
        /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
        pub fn request_invoice(
                &self, metadata: Vec<u8>, payer_id: PublicKey
-       ) -> Result<InvoiceRequestBuilder, SemanticError> {
+       ) -> Result<InvoiceRequestBuilder<ExplicitPayerId, secp256k1::SignOnly>, SemanticError> {
                if self.features().requires_unknown_bits() {
                        return Err(SemanticError::UnknownRequiredFeatures);
                }
index 12b471c6ce4ee4e36934d0962becb7e5f8c51f2b..7609c4666197ce3c91f685811b12ca0bc34f718a 100644 (file)
@@ -9,6 +9,7 @@
 
 //! Data structures and encoding for `invoice_request_metadata` records.
 
+use crate::offers::signer::Metadata;
 use crate::util::ser::WithoutLength;
 
 use crate::prelude::*;
@@ -19,7 +20,7 @@ use crate::prelude::*;
 /// [`InvoiceRequest::payer_id`]: crate::offers::invoice_request::InvoiceRequest::payer_id
 #[derive(Clone, Debug)]
 #[cfg_attr(test, derive(PartialEq))]
-pub(super) struct PayerContents(pub Vec<u8>);
+pub(super) struct PayerContents(pub Metadata);
 
 tlv_stream!(PayerTlvStream, PayerTlvStreamRef, 0..1, {
        (0, metadata: (Vec<u8>, WithoutLength)),
index 6d44eb3da6d54e4a5e505a70d1aeee7316fd53cf..999d68c448c1b1f56e80ab0373aa0545592f0541 100644 (file)
@@ -86,6 +86,7 @@ use crate::offers::invoice_request::{InvoiceRequestTlvStream, InvoiceRequestTlvS
 use crate::offers::offer::{OfferTlvStream, OfferTlvStreamRef};
 use crate::offers::parse::{Bech32Encode, ParseError, ParsedMessage, SemanticError};
 use crate::offers::payer::{PayerContents, PayerTlvStream, PayerTlvStreamRef};
+use crate::offers::signer::Metadata;
 use crate::onion_message::BlindedPath;
 use crate::util::ser::{SeekReadable, WithoutLength, Writeable, Writer};
 use crate::util::string::PrintableString;
@@ -117,6 +118,7 @@ impl RefundBuilder {
                        return Err(SemanticError::InvalidAmount);
                }
 
+               let metadata = Metadata::Bytes(metadata);
                let refund = RefundContents {
                        payer: PayerContents(metadata), description, absolute_expiry: None, issuer: None,
                        paths: None, chain: None, amount_msats, features: InvoiceRequestFeatures::empty(),
@@ -281,7 +283,7 @@ impl Refund {
        ///
        /// [`payer_id`]: Self::payer_id
        pub fn metadata(&self) -> &[u8] {
-               &self.contents.payer.0
+               &self.contents.payer.0.as_bytes().unwrap()[..]
        }
 
        /// A chain that the refund is valid for.
@@ -405,7 +407,7 @@ impl RefundContents {
 
        pub(super) fn as_tlv_stream(&self) -> RefundTlvStreamRef {
                let payer = PayerTlvStreamRef {
-                       metadata: Some(&self.payer.0),
+                       metadata: self.payer.0.as_bytes(),
                };
 
                let offer = OfferTlvStreamRef {
@@ -509,7 +511,7 @@ impl TryFrom<RefundTlvStream> for RefundContents {
 
                let payer = match payer_metadata {
                        None => return Err(SemanticError::MissingPayerMetadata),
-                       Some(metadata) => PayerContents(metadata),
+                       Some(metadata) => PayerContents(Metadata::Bytes(metadata)),
                };
 
                if metadata.is_some() {
index 2ee3d13afbb9c173c1c9bf03654d392e7fa5f450..a8ea941e3be834e3504af6c23643db5219e11e33 100644 (file)
@@ -96,6 +96,12 @@ impl Metadata {
        }
 }
 
+impl Default for Metadata {
+       fn default() -> Self {
+               Metadata::Bytes(vec![])
+       }
+}
+
 impl fmt::Debug for Metadata {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                match self {