]> git.bitcoin.ninja Git - rust-lightning/commitdiff
Invoice encoding and parsing
authorJeffrey Czyz <jkczyz@gmail.com>
Mon, 12 Sep 2022 14:30:06 +0000 (09:30 -0500)
committerJeffrey Czyz <jkczyz@gmail.com>
Fri, 20 Jan 2023 22:04:37 +0000 (16:04 -0600)
Define an interface for BOLT 12 `invoice` messages. The underlying
format consists of the original bytes and the parsed contents.

The bytes are later needed for serialization. This is because it must
mirror all the `offer` and `invoice_request` TLV records, including
unknown ones, which aren't represented in the contents.

Invoices may be created for an Offer (from an InvoiceRequest) or for a
Refund. The primary difference is how the signing pubkey is given -- by
the writer of the offer or the reader of the refund.

lightning/src/offers/invoice.rs [new file with mode: 0644]
lightning/src/offers/invoice_request.rs
lightning/src/offers/mod.rs
lightning/src/offers/offer.rs
lightning/src/offers/parse.rs
lightning/src/offers/refund.rs

diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs
new file mode 100644 (file)
index 0000000..8264aa1
--- /dev/null
@@ -0,0 +1,391 @@
+// This file is Copyright its original authors, visible in version control
+// history.
+//
+// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
+// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
+// You may not use this file except in accordance with one or both of these
+// licenses.
+
+//! Data structures and encoding for `invoice` messages.
+
+use bitcoin::blockdata::constants::ChainHash;
+use bitcoin::network::constants::Network;
+use bitcoin::secp256k1::PublicKey;
+use bitcoin::secp256k1::schnorr::Signature;
+use bitcoin::util::address::{Address, Payload, WitnessVersion};
+use core::convert::TryFrom;
+use core::time::Duration;
+use crate::io;
+use crate::ln::PaymentHash;
+use crate::ln::features::{BlindedHopFeatures, Bolt12InvoiceFeatures};
+use crate::ln::msgs::DecodeError;
+use crate::offers::invoice_request::{InvoiceRequestContents, InvoiceRequestTlvStream};
+use crate::offers::merkle::{SignatureTlvStream, self};
+use crate::offers::offer::OfferTlvStream;
+use crate::offers::parse::{ParseError, ParsedMessage, SemanticError};
+use crate::offers::payer::PayerTlvStream;
+use crate::offers::refund::RefundContents;
+use crate::onion_message::BlindedPath;
+use crate::util::ser::{HighZeroBytesDroppedBigSize, SeekReadable, WithoutLength, Writeable, Writer};
+
+use crate::prelude::*;
+
+#[cfg(feature = "std")]
+use std::time::SystemTime;
+
+const DEFAULT_RELATIVE_EXPIRY: Duration = Duration::from_secs(7200);
+
+const SIGNATURE_TAG: &'static str = concat!("lightning", "invoice", "signature");
+
+/// An `Invoice` is a payment request, typically corresponding to an [`Offer`] or a [`Refund`].
+///
+/// An invoice may be sent in response to an [`InvoiceRequest`] in the case of an offer or sent
+/// directly after scanning a refund. It includes all the information needed to pay a recipient.
+///
+/// [`Offer`]: crate::offers::offer::Offer
+/// [`Refund`]: crate::offers::refund::Refund
+/// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+pub struct Invoice {
+       bytes: Vec<u8>,
+       contents: InvoiceContents,
+       signature: Signature,
+}
+
+/// The contents of an [`Invoice`] for responding to either an [`Offer`] or a [`Refund`].
+///
+/// [`Offer`]: crate::offers::offer::Offer
+/// [`Refund`]: crate::offers::refund::Refund
+enum InvoiceContents {
+       /// Contents for an [`Invoice`] corresponding to an [`Offer`].
+       ///
+       /// [`Offer`]: crate::offers::offer::Offer
+       ForOffer {
+               invoice_request: InvoiceRequestContents,
+               fields: InvoiceFields,
+       },
+       /// Contents for an [`Invoice`] corresponding to a [`Refund`].
+       ///
+       /// [`Refund`]: crate::offers::refund::Refund
+       ForRefund {
+               refund: RefundContents,
+               fields: InvoiceFields,
+       },
+}
+
+/// Invoice-specific fields for an `invoice` message.
+struct InvoiceFields {
+       payment_paths: Vec<(BlindedPath, BlindedPayInfo)>,
+       created_at: Duration,
+       relative_expiry: Option<Duration>,
+       payment_hash: PaymentHash,
+       amount_msats: u64,
+       fallbacks: Option<Vec<FallbackAddress>>,
+       features: Bolt12InvoiceFeatures,
+       signing_pubkey: PublicKey,
+}
+
+impl Invoice {
+       /// Paths to the recipient originating from publicly reachable nodes, including information
+       /// needed for routing payments across them. Blinded paths provide recipient privacy by
+       /// obfuscating its node id.
+       pub fn payment_paths(&self) -> &[(BlindedPath, BlindedPayInfo)] {
+               &self.contents.fields().payment_paths[..]
+       }
+
+       /// Duration since the Unix epoch when the invoice was created.
+       pub fn created_at(&self) -> Duration {
+               self.contents.fields().created_at
+       }
+
+       /// Duration since [`Invoice::created_at`] when the invoice has expired and therefore should no
+       /// longer be paid.
+       pub fn relative_expiry(&self) -> Duration {
+               self.contents.fields().relative_expiry.unwrap_or(DEFAULT_RELATIVE_EXPIRY)
+       }
+
+       /// Whether the invoice has expired.
+       #[cfg(feature = "std")]
+       pub fn is_expired(&self) -> bool {
+               let absolute_expiry = self.created_at().checked_add(self.relative_expiry());
+               match absolute_expiry {
+                       Some(seconds_from_epoch) => match SystemTime::UNIX_EPOCH.elapsed() {
+                               Ok(elapsed) => elapsed > seconds_from_epoch,
+                               Err(_) => false,
+                       },
+                       None => false,
+               }
+       }
+
+       /// SHA256 hash of the payment preimage that will be given in return for paying the invoice.
+       pub fn payment_hash(&self) -> PaymentHash {
+               self.contents.fields().payment_hash
+       }
+
+       /// The minimum amount required for a successful payment of the invoice.
+       pub fn amount_msats(&self) -> u64 {
+               self.contents.fields().amount_msats
+       }
+
+       /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to
+       /// least-preferred.
+       pub fn fallbacks(&self) -> Vec<Address> {
+               let network = match self.network() {
+                       None => return Vec::new(),
+                       Some(network) => network,
+               };
+
+               let to_valid_address = |address: &FallbackAddress| {
+                       let version = match WitnessVersion::try_from(address.version) {
+                               Ok(version) => version,
+                               Err(_) => return None,
+                       };
+
+                       let program = &address.program;
+                       if program.len() < 2 || program.len() > 40 {
+                               return None;
+                       }
+
+                       let address = Address {
+                               payload: Payload::WitnessProgram {
+                                       version,
+                                       program: address.program.clone(),
+                               },
+                               network,
+                       };
+
+                       if !address.is_standard() && version == WitnessVersion::V0 {
+                               return None;
+                       }
+
+                       Some(address)
+               };
+
+               self.contents.fields().fallbacks
+                       .as_ref()
+                       .map(|fallbacks| fallbacks.iter().filter_map(to_valid_address).collect())
+                       .unwrap_or_else(Vec::new)
+       }
+
+       fn network(&self) -> Option<Network> {
+               let chain = self.contents.chain();
+               if chain == ChainHash::using_genesis_block(Network::Bitcoin) {
+                       Some(Network::Bitcoin)
+               } else if chain == ChainHash::using_genesis_block(Network::Testnet) {
+                       Some(Network::Testnet)
+               } else if chain == ChainHash::using_genesis_block(Network::Signet) {
+                       Some(Network::Signet)
+               } else if chain == ChainHash::using_genesis_block(Network::Regtest) {
+                       Some(Network::Regtest)
+               } else {
+                       None
+               }
+       }
+
+       /// Features pertaining to paying an invoice.
+       pub fn features(&self) -> &Bolt12InvoiceFeatures {
+               &self.contents.fields().features
+       }
+
+       /// The public key used to sign invoices.
+       pub fn signing_pubkey(&self) -> PublicKey {
+               self.contents.fields().signing_pubkey
+       }
+
+       /// Signature of the invoice using [`Invoice::signing_pubkey`].
+       pub fn signature(&self) -> Signature {
+               self.signature
+       }
+}
+
+impl InvoiceContents {
+       fn chain(&self) -> ChainHash {
+               match self {
+                       InvoiceContents::ForOffer { invoice_request, .. } => invoice_request.chain(),
+                       InvoiceContents::ForRefund { refund, .. } => refund.chain(),
+               }
+       }
+
+       fn fields(&self) -> &InvoiceFields {
+               match self {
+                       InvoiceContents::ForOffer { fields, .. } => fields,
+                       InvoiceContents::ForRefund { fields, .. } => fields,
+               }
+       }
+}
+
+impl Writeable for Invoice {
+       fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> {
+               WithoutLength(&self.bytes).write(writer)
+       }
+}
+
+impl TryFrom<Vec<u8>> for Invoice {
+       type Error = ParseError;
+
+       fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
+               let parsed_invoice = ParsedMessage::<FullInvoiceTlvStream>::try_from(bytes)?;
+               Invoice::try_from(parsed_invoice)
+       }
+}
+
+tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef, 160..240, {
+       (160, paths: (Vec<BlindedPath>, WithoutLength)),
+       (162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength)),
+       (164, created_at: (u64, HighZeroBytesDroppedBigSize)),
+       (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)),
+       (168, payment_hash: PaymentHash),
+       (170, amount: (u64, HighZeroBytesDroppedBigSize)),
+       (172, fallbacks: (Vec<FallbackAddress>, WithoutLength)),
+       (174, features: (Bolt12InvoiceFeatures, WithoutLength)),
+       (176, node_id: PublicKey),
+});
+
+/// Information needed to route a payment across a [`BlindedPath`] hop.
+#[derive(Debug, PartialEq)]
+pub struct BlindedPayInfo {
+       fee_base_msat: u32,
+       fee_proportional_millionths: u32,
+       cltv_expiry_delta: u16,
+       htlc_minimum_msat: u64,
+       htlc_maximum_msat: u64,
+       features: BlindedHopFeatures,
+}
+
+impl_writeable!(BlindedPayInfo, {
+       fee_base_msat,
+       fee_proportional_millionths,
+       cltv_expiry_delta,
+       htlc_minimum_msat,
+       htlc_maximum_msat,
+       features
+});
+
+/// Wire representation for an on-chain fallback address.
+#[derive(Debug, PartialEq)]
+pub(super) struct FallbackAddress {
+       version: u8,
+       program: Vec<u8>,
+}
+
+impl_writeable!(FallbackAddress, { version, program });
+
+type FullInvoiceTlvStream =
+       (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream, SignatureTlvStream);
+
+impl SeekReadable for FullInvoiceTlvStream {
+       fn read<R: io::Read + io::Seek>(r: &mut R) -> Result<Self, DecodeError> {
+               let payer = SeekReadable::read(r)?;
+               let offer = SeekReadable::read(r)?;
+               let invoice_request = SeekReadable::read(r)?;
+               let invoice = SeekReadable::read(r)?;
+               let signature = SeekReadable::read(r)?;
+
+               Ok((payer, offer, invoice_request, invoice, signature))
+       }
+}
+
+type PartialInvoiceTlvStream =
+       (PayerTlvStream, OfferTlvStream, InvoiceRequestTlvStream, InvoiceTlvStream);
+
+impl TryFrom<ParsedMessage<FullInvoiceTlvStream>> for Invoice {
+       type Error = ParseError;
+
+       fn try_from(invoice: ParsedMessage<FullInvoiceTlvStream>) -> Result<Self, Self::Error> {
+               let ParsedMessage { bytes, tlv_stream } = invoice;
+               let (
+                       payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream,
+                       SignatureTlvStream { signature },
+               ) = tlv_stream;
+               let contents = InvoiceContents::try_from(
+                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream, invoice_tlv_stream)
+               )?;
+
+               let signature = match signature {
+                       None => return Err(ParseError::InvalidSemantics(SemanticError::MissingSignature)),
+                       Some(signature) => signature,
+               };
+               let pubkey = contents.fields().signing_pubkey;
+               merkle::verify_signature(&signature, SIGNATURE_TAG, &bytes, pubkey)?;
+
+               Ok(Invoice { bytes, contents, signature })
+       }
+}
+
+impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents {
+       type Error = SemanticError;
+
+       fn try_from(tlv_stream: PartialInvoiceTlvStream) -> Result<Self, Self::Error> {
+               let (
+                       payer_tlv_stream,
+                       offer_tlv_stream,
+                       invoice_request_tlv_stream,
+                       InvoiceTlvStream {
+                               paths, blindedpay, created_at, relative_expiry, payment_hash, amount, fallbacks,
+                               features, node_id,
+                       },
+               ) = tlv_stream;
+
+               let payment_paths = match (paths, blindedpay) {
+                       (None, _) => return Err(SemanticError::MissingPaths),
+                       (_, None) => return Err(SemanticError::InvalidPayInfo),
+                       (Some(paths), _) if paths.is_empty() => return Err(SemanticError::MissingPaths),
+                       (Some(paths), Some(blindedpay)) if paths.len() != blindedpay.len() => {
+                               return Err(SemanticError::InvalidPayInfo);
+                       },
+                       (Some(paths), Some(blindedpay)) => {
+                               paths.into_iter().zip(blindedpay.into_iter()).collect::<Vec<_>>()
+                       },
+               };
+
+               let created_at = match created_at {
+                       None => return Err(SemanticError::MissingCreationTime),
+                       Some(timestamp) => Duration::from_secs(timestamp),
+               };
+
+               let relative_expiry = relative_expiry
+                       .map(Into::<u64>::into)
+                       .map(Duration::from_secs);
+
+               let payment_hash = match payment_hash {
+                       None => return Err(SemanticError::MissingPaymentHash),
+                       Some(payment_hash) => payment_hash,
+               };
+
+               let amount_msats = match amount {
+                       None => return Err(SemanticError::MissingAmount),
+                       Some(amount) => amount,
+               };
+
+               let features = features.unwrap_or_else(Bolt12InvoiceFeatures::empty);
+
+               let signing_pubkey = match node_id {
+                       None => return Err(SemanticError::MissingSigningPubkey),
+                       Some(node_id) => node_id,
+               };
+
+               let fields = InvoiceFields {
+                       payment_paths, created_at, relative_expiry, payment_hash, amount_msats, fallbacks,
+                       features, signing_pubkey,
+               };
+
+               match offer_tlv_stream.node_id {
+                       Some(expected_signing_pubkey) => {
+                               if fields.signing_pubkey != expected_signing_pubkey {
+                                       return Err(SemanticError::InvalidSigningPubkey);
+                               }
+
+                               let invoice_request = InvoiceRequestContents::try_from(
+                                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
+                               )?;
+                               Ok(InvoiceContents::ForOffer { invoice_request, fields })
+                       },
+                       None => {
+                               let refund = RefundContents::try_from(
+                                       (payer_tlv_stream, offer_tlv_stream, invoice_request_tlv_stream)
+                               )?;
+                               Ok(InvoiceContents::ForRefund { refund, fields })
+                       },
+               }
+       }
+}
index fd5ecda558af523f1664452f98968edef311da9c..21c11bcbfc6b2429b078807e5b8682fe02667f8a 100644 (file)
 //!
 //! An [`InvoiceRequest`] can be built from a parsed [`Offer`] as an "offer to be paid". It is
 //! typically constructed by a customer and sent to the merchant who had published the corresponding
-//! offer. The recipient of the request responds with an `Invoice`.
+//! offer. The recipient of the request responds with an [`Invoice`].
 //!
 //! For an "offer for money" (e.g., refund, ATM withdrawal), where an offer doesn't exist as a
 //! precursor, see [`Refund`].
 //!
+//! [`Invoice`]: crate::offers::invoice::Invoice
 //! [`Refund`]: crate::offers::refund::Refund
 //!
 //! ```ignore
@@ -239,11 +240,12 @@ impl<'a> UnsignedInvoiceRequest<'a> {
        }
 }
 
-/// An `InvoiceRequest` is a request for an `Invoice` formulated from an [`Offer`].
+/// An `InvoiceRequest` is a request for an [`Invoice`] formulated from an [`Offer`].
 ///
 /// An offer may provide choices such as quantity, amount, chain, features, etc. An invoice request
 /// specifies these such that its recipient can send an invoice for payment.
 ///
+/// [`Invoice`]: crate::offers::invoice::Invoice
 /// [`Offer`]: crate::offers::offer::Offer
 #[derive(Clone, Debug)]
 pub struct InvoiceRequest {
@@ -252,11 +254,13 @@ pub struct InvoiceRequest {
        signature: Signature,
 }
 
-/// The contents of an [`InvoiceRequest`], which may be shared with an `Invoice`.
+/// The contents of an [`InvoiceRequest`], which may be shared with an [`Invoice`].
+///
+/// [`Invoice`]: crate::offers::invoice::Invoice
 #[derive(Clone, Debug)]
 pub(super) struct InvoiceRequestContents {
        payer: PayerContents,
-       offer: OfferContents,
+       pub(super) offer: OfferContents,
        chain: Option<ChainHash>,
        amount_msats: Option<u64>,
        features: InvoiceRequestFeatures,
@@ -327,7 +331,7 @@ impl InvoiceRequest {
 }
 
 impl InvoiceRequestContents {
-       fn chain(&self) -> ChainHash {
+       pub(super) fn chain(&self) -> ChainHash {
                self.chain.unwrap_or_else(|| self.offer.implied_chain())
        }
 
index 11df5ca1f8a108457fe1df9864ec8c0a916fdbe8..2da6fac08ff929e788af18ad1b8970ad71388730 100644 (file)
@@ -12,6 +12,7 @@
 //!
 //! Offers are a flexible protocol for Lightning payments.
 
+pub mod invoice;
 pub mod invoice_request;
 mod merkle;
 pub mod offer;
index e655004460024acd7afe2971bc9556392c405a0b..d28a0bdd90ef3041613620972d09373a146764c1 100644 (file)
@@ -232,7 +232,7 @@ impl OfferBuilder {
 /// An `Offer` is a potentially long-lived proposal for payment of a good or service.
 ///
 /// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a
-/// customer may request an `Invoice` for a specific quantity and using an amount sufficient to
+/// customer may request an [`Invoice`] for a specific quantity and using an amount sufficient to
 /// cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`].
 ///
 /// Offers may be denominated in currency other than bitcoin but are ultimately paid using the
@@ -241,6 +241,7 @@ impl OfferBuilder {
 /// Through the use of [`BlindedPath`]s, offers provide recipient privacy.
 ///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Invoice`]: crate::offers::invoice::Invoice
 #[derive(Clone, Debug)]
 pub struct Offer {
        // The serialized offer. Needed when creating an `InvoiceRequest` if the offer contains unknown
@@ -249,9 +250,10 @@ pub struct Offer {
        pub(super) contents: OfferContents,
 }
 
-/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an `Invoice`.
+/// The contents of an [`Offer`], which may be shared with an [`InvoiceRequest`] or an [`Invoice`].
 ///
 /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
+/// [`Invoice`]: crate::offers::invoice::Invoice
 #[derive(Clone, Debug)]
 pub(super) struct OfferContents {
        chains: Option<Vec<ChainHash>>,
@@ -359,7 +361,7 @@ impl Offer {
 
        /// The public key used by the recipient to sign invoices.
        pub fn signing_pubkey(&self) -> PublicKey {
-               self.contents.signing_pubkey
+               self.contents.signing_pubkey()
        }
 
        /// Creates an [`InvoiceRequest`] for the offer with the given `metadata` and `payer_id`, which
@@ -473,6 +475,10 @@ impl OfferContents {
                }
        }
 
+       pub(super) fn signing_pubkey(&self) -> PublicKey {
+               self.signing_pubkey
+       }
+
        pub(super) fn as_tlv_stream(&self) -> OfferTlvStreamRef {
                let (currency, amount) = match &self.amount {
                        None => (None, None),
index deada66b05c2549ad29ca2b77459e6c6779b0346..a7d13e57050379d7982c819501186708d79cf34c 100644 (file)
@@ -147,6 +147,8 @@ pub enum SemanticError {
        MissingDescription,
        /// A signing pubkey was not provided.
        MissingSigningPubkey,
+       /// A signing pubkey was provided but a different one was expected.
+       InvalidSigningPubkey,
        /// A signing pubkey was provided but was not expected.
        UnexpectedSigningPubkey,
        /// A quantity was expected but was missing.
@@ -159,6 +161,14 @@ pub enum SemanticError {
        MissingPayerMetadata,
        /// A payer id was expected but was missing.
        MissingPayerId,
+       /// Blinded paths were expected but were missing.
+       MissingPaths,
+       /// The blinded payinfo given does not match the number of blinded path hops.
+       InvalidPayInfo,
+       /// An invoice creation time was expected but was missing.
+       MissingCreationTime,
+       /// An invoice payment hash was expected but was missing.
+       MissingPaymentHash,
        /// A signature was expected but was missing.
        MissingSignature,
 }
index d3798463b440ed6c0bead3034392671aa57b5555..f864e73a3d37f9d7052f5a818d7e176ebc2c7464 100644 (file)
 //! Data structures and encoding for refunds.
 //!
 //! A [`Refund`] is an "offer for money" and is typically constructed by a merchant and presented
-//! directly to the customer. The recipient responds with an `Invoice` to be paid.
+//! directly to the customer. The recipient responds with an [`Invoice`] to be paid.
 //!
 //! This is an [`InvoiceRequest`] produced *not* in response to an [`Offer`].
 //!
+//! [`Invoice`]: crate::offers::invoice::Invoice
 //! [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest
 //! [`Offer`]: crate::offers::offer::Offer
 //!
@@ -191,12 +192,13 @@ impl RefundBuilder {
        }
 }
 
-/// A `Refund` is a request to send an `Invoice` without a preceding [`Offer`].
+/// A `Refund` is a request to send an [`Invoice`] without a preceding [`Offer`].
 ///
 /// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to
 /// recoup their funds. A refund may be used more generally as an "offer for money", such as with a
 /// bitcoin ATM.
 ///
+/// [`Invoice`]: crate::offers::invoice::Invoice
 /// [`Offer`]: crate::offers::offer::Offer
 #[derive(Clone, Debug)]
 pub struct Refund {
@@ -204,9 +206,11 @@ pub struct Refund {
        contents: RefundContents,
 }
 
-/// The contents of a [`Refund`], which may be shared with an `Invoice`.
+/// The contents of a [`Refund`], which may be shared with an [`Invoice`].
+///
+/// [`Invoice`]: crate::offers::invoice::Invoice
 #[derive(Clone, Debug)]
-struct RefundContents {
+pub(super) struct RefundContents {
        payer: PayerContents,
        // offer fields
        metadata: Option<Vec<u8>>,
@@ -311,7 +315,7 @@ impl AsRef<[u8]> for Refund {
 }
 
 impl RefundContents {
-       fn chain(&self) -> ChainHash {
+       pub(super) fn chain(&self) -> ChainHash {
                self.chain.unwrap_or_else(|| self.implied_chain())
        }