Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consider dropping non_exhaustive on Network, etc #2225

Open
TheBlueMatt opened this issue Nov 26, 2023 · 65 comments
Open

Consider dropping non_exhaustive on Network, etc #2225

TheBlueMatt opened this issue Nov 26, 2023 · 65 comments

Comments

@TheBlueMatt
Copy link
Member

In 0.30, #[non_exhaustive] was added to the Network enum in what I assume was a step towards stabilization with forwards compatibility. #[non_exhaustive] doesn't, itself, provide forwards compatibility with minor version changes, but rather forces downstream devs to think about it, and puts the burden for forwards compatibility on them. This only works if downstream projects have to be able to have some kind of sensible default behavior for "unknown future bitcoin-style network", which isn't super practical. For example, this came up in the lightning-invoice crate, we have to convert from the rust-bitcoin Network to a BOLT11-specific Currency - we cannot reasonably have forward-compatible code that will "just work" if a new enum variant is added, so an unreachable was added. That will obviously be removed, but there's really nothing we can do to be forwards compatible here, and instead would strongly prefer to see a version bump of rust-bitcoin so that we can update our code to handle entire new bitcoin-compatible networks.

@apoelstra
Copy link
Member

Specifically with Network we've moved away from an open-ended enum to just a "mainnet/testnet" dichotomy. So definitely +1 to removing #[non_exhaustive] there.

For other types, I tend to agree with you. I think we should leave #[non_exhaustive] on errors (which we expect people will rarely/never match on, and usually only match on a single variant that they care about), and for everything else we should think hard about what we actually expect people to do with unknown variants. And if the answer is "panic" we shouldn't make them do that.

TheBlueMatt added a commit to TheBlueMatt/rust-lightning that referenced this issue Nov 26, 2023
`rust-bitcoin` 0.30 added `#[non_exhaustive]` to the `Network`
enum, allowing them to "add support" for a new network type without
a major version change in the future. When upgrading, we added a
simple `unreachable` for the general match arm, which would break
in a minor version change of `rust-bitcoin`.

While it seems [possible rust-bitcoin will change
this](rust-bitcoin/rust-bitcoin#2225),
we still shouldn't ba panicking, which we drop here in favor of a
`debug_assert`ion, and a default value.
@tnull
Copy link
Contributor

tnull commented Nov 27, 2023

As a side note, I want to add that the addition of non_exhaustive leads to issues when trying to expose rust-bitcoin types in UniFFI-generated bindings, as the latter currently doesn't support non_exhaustive as of yet. Projects in the rust-bitcoin space using UniFFI (BDK, LDK Node, Fedimint, ...) therefore are currently forced to wrap the corresponding enums in newtypes which is cumbersome and also breaks interop between crates as each crate might create its own newtype.

FWIW, as it's generally a bit of a nuisance that dependent crates currently need to create incompatible rust-bitcoin newtype @thunderbiscuit and I previously discussed creating Uniffi bindings for rust-bitcoin itself which dependent crates then could reuse without reinventing their own incompatible types. However, there are currently a number of blockers that keep us from doing this cleanly, one of them being the usage of non_exhaustive.

tcharding added a commit to tcharding/rust-bitcoin that referenced this issue Nov 27, 2023
Recently we added the `non_exhaustive` attribute to `Network` in an
attempt to make the type forward compatible. This has proven to be an
annoyance to downstream users because there is no obvious recourse for
the unknown variant and matching on `Network` is common.

Note that if/when a new network is added by Core it is a big deal so we
can and should bump our version when we add support for it.

Remove `#[non_exhaustive]` from the `Network` type.

Fix: #rust-bitcoin#2225
tcharding added a commit to tcharding/rust-bitcoin that referenced this issue Nov 28, 2023
Recently we added the `non_exhaustive` attribute to `Network` in an
attempt to make the type forward compatible. This has proven to be an
annoyance to downstream users because there is no obvious recourse for
the unknown variant and matching on `Network` is common.

Note that if/when a new network is added by Core it is a big deal so we
can and should bump our version when we add support for it.

Remove `#[non_exhaustive]` from the `Network` type.

Fix: rust-bitcoin#2225
@clarkmoody
Copy link
Member

Agreed on removal where it makes sense. I'm not a huge fan of non_exhaustive generally, but it does make sense for parts of a library API.

Sharmalm added a commit to Sharmalm/rust-lightning that referenced this issue Dec 1, 2023
Clean up typos and unused variables/imports

Refactor check_closed_event for multiple events

The check_closed_event function verified closure events against multiple
counterparty nodes, but only a single closure reason and channel
capacity. This commit introduces a check_closed_events function to
verify events against descriptions of each expected event, and refactors
check_closed_event in function of check_closed_events.

Construct ShutdownResult as a struct in Channel

This refactors ShutdownResult as follows:
- Makes ShutdownResult a struct instead of a tuple to represent
  individual results that need to be handled. This recently also
  includes funding batch closure propagations.
- Makes Channel solely responsible for constructing ShutdownResult as
  it should own all channel-specific logic.

Add is_expired_no_std to Offer & Refund

This was available for OfferContents but not an Offer so dependent
projects could not access it.

Remove channel monitor sync in progress log

This log is super spammy for us and isn't very useful.

next_hop_pubkey secp Verification only

Update docs on `MonitorEvent::HolderForceClosed`

In a96e2fe we renamed
`MonitorEvent::CommitmentTxConfirmed` to `HolderForceClosed` to
better document what actually happened. However, we failed to
update the documentation on the type, which we do here.

Pointed out by @yellowred.

changing branch

commits

Derived debug

solving fuzz test problem

solved fuzz problem

Wrap long onion_message fuzz strings

Some editors like vim slow to a crawl when scrolling over long strings
when syntax highlighting is turned on. Limit the length in fuzz strings
to avoid this.

Re-add one-hop onion message fuzzing test

Revert fuzz test removal in 6dc4223.
The test originally checked that OnionMessenger would fail for one-hop
blinded paths. The commit added support for such paths, but changing the
checks was not sufficient since the node was not connected to the
introduction node of the reply path. This is required in order to work
with the trivial TestMessageRouter. Fix this by explicitly connecting
the nodes.

Move bech32 parsing tests to the parse module

Additional BOLT 12 tests specific to Offer were added, which will live
in the offer module. Thus, it makes sense to move the bech32 tests to
the parse module.

Separate and describe BOLT 12 test vectors

BOLT 12 test vectors for offer parsing

One discrepancy from the spec still needs to be resolved:

https://github.com/lightning/bolts/pull/798/files#r1334851959

add preflight probes test coverage

Handling for sign_counterparty_commitment failing during normal op

If sign_counterparty_commitment fails (i.e. because the signer is
temporarily disconnected), this really indicates that we should
retry the message sending later, rather than force-closing the
channel (which probably won't even work if the signer is missing).

Here we add initial handling of sign_counterparty_commitment
failing during normal channel operation, setting a new flag in
`ChannelContext` which indicates we should retry sending the
commitment update later. We don't yet add any ability to do that
retry.

Handle sign_counterparty_commitment failing during outb funding

If sign_counterparty_commitment fails (i.e. because the signer is
temporarily disconnected), this really indicates that we should
retry the message sending which required the signature later,
rather than force-closing the channel (which probably won't even
work if the signer is missing).

Here we add initial handling of sign_counterparty_commitment
failing during outbound channel funding, setting a new flag in
`ChannelContext` which indicates we should retry sending the
`funding_created` later. We don't yet add any ability to do that
retry.

Handle sign_counterparty_commitment failing during inb funding

If sign_counterparty_commitment fails (i.e. because the signer is
temporarily disconnected), this really indicates that we should
retry the message sending which required the signature later,
rather than force-closing the channel (which probably won't even
work if the signer is missing).

Here we add initial handling of sign_counterparty_commitment
failing during inbound channel funding, setting a flag in
`ChannelContext` which indicates we should retry sending the
`funding_signed` later. We don't yet add any ability to do that
retry.

Handle retrying sign_counterparty_commitment failures

If sign_counterparty_commitment fails (i.e. because the signer is
temporarily disconnected), this really indicates that we should
retry the message sending which required the signature later,
rather than force-closing the channel (which probably won't even
work if the signer is missing).

This commit adds initial retrying of failures, specifically
regenerating commitment updates, attempting to re-sign the
`CommitmentSigned` message, and sending it to our peers if we
succed.

Handle retrying sign_counterparty_commitment outb funding failures

If sign_counterparty_commitment fails (i.e. because the signer is
temporarily disconnected), this really indicates that we should
retry the message sending which required the signature later,
rather than force-closing the channel (which probably won't even
work if the signer is missing).

This commit adds retrying of outbound funding_created signing
failures, regenerating the `FundingCreated` message, attempting to
re-sign, and sending it to our peers if we succeed.

Handle retrying sign_counterparty_commitment inb funding failures

If sign_counterparty_commitment fails (i.e. because the signer is
temporarily disconnected), this really indicates that we should
retry the message sending which required the signature later,
rather than force-closing the channel (which probably won't even
work if the signer is missing).

This commit adds retrying of inbound funding_created signing
failures, regenerating the `FundingSigned` message, attempting to
re-sign, and sending it to our peers if we succeed.

Add basic async signer tests

Adds a `get_signer` method to the context so that a test can get ahold of the
channel signer. Adds a `set_available` method on the `TestChannelSigner` to
allow a test to enable and disable the signer: when disabled some of the
signer's methods will return `Err` which will typically activate the error
handling case. Adds a `set_channel_signer_available` function on the test
`Node` class to make it easy to enable and disable a specific signer.

Adds a new `async_signer_tests` module:

* Check for asynchronous handling of `funding_created` and `funding_signed`.
* Check that we correctly resume processing after awaiting an asynchronous
  signature for a `commitment_signed` event.
* Verify correct handling during peer disconnect.
* Verify correct handling for inbound zero-conf.

Added `temporary_channel_id` to `create_channel`.

By default, LDK will generate the initial temporary channel ID for you.
However, in certain cases, it's desirable to have a temporary channel ID
specified by the caller in case of any pre-negotiation that needs to
happen between peers prior to the channel open message. For example, LND
has a `FundingShim` API that allows for advanced funding flows based on
the temporary channel ID of the channel.

This patch adds support for optionally specifying the temporary channel
ID of the channel through the `create_channel` API.

public create_payment_onion in onion_utils

export create_onion_message and peel_onion_message from ln::onion_message

refactor to remove message_digest

We change the Bolt12Invoice struct to carry a tagged hash. Because
message_digest is then only used in one place, we can inline it in
the TaggedHash constructor.

expose more granular data in TaggedHash struct

Expose tag and merkle root fields in the TaggedHash struct.

Avoid an unnecessary allocation in `TaggedHash`

A well-formed tag is always a constant, so allocating to store it
is unnecessary when we can just make the tag a `&'static str`.

Update fuzzing instructions for libFuzzer/cargo-fuzz

Fix potential cases where max_dust_htlc_exposure_msat overflows

Include counterparty skimmed fees in PaymentClaimed event.

Link to LSP spec in accept_underpaying_htlcs config

Reduce on-startup heap frag due to network graph map/vec doubling

When we're reading a `NetworkGraph`, we know how many
nodes/channels we are reading, there's no reason not to
pre-allocate the `IndexedMap`'s inner `HashMap` and `Vec`, which we
do here.

This seems to reduce on-startup heap fragmentation with glibc by
something like 100MiB.

Prefer `Writeable.encode()` over `VecWriter` use

It does the same thing and its much simpler.

Pre-allocate send buffer when forwarding gossip

When forwarding gossip, rather than relying on Vec doubling,
pre-allocate the message encoding buffer.

Avoid unnecessarily overriding `serialized_length`

...as LLVM will handle it just fine for us, in most cases.

Pre-allocate the full `Vec` prior to serializing as a `Vec<u8>`

We end up generating a substantial amount of allocations just
doubling `Vec`s when serializing to them, and our
`serialized_length` method is generally rather effecient, so we
just rely on it and allocate correctly up front.

Add an option to in-place decrypt with `ChaCha20Poly1305`

In the next commit we'll use this to avoid an allocation when
deserializing messages from the wire.

Avoid unnecessarily alloc'ing a new buffer when decrypting messages

When decrypting P2P messages, we already have a read buffer that we
read the message into. There's no reason to allocate a new `Vec` to
store the decrypted message when we can just overwrite the read
buffer and call it a day.

Use `VecDeque`, rather than `LinkedList` in peer message buffering

When buffering outbound messages for peers, `LinkedList` adds
rather substantial allocation overhead, which we avoid here by
swapping for a `VecDeque`.

Avoid re-allocating to encrypt gossip messages when forwarding

When we forward gossip messages, we store them in a separate buffer
before we encrypt them (and commit to the order in which they'll
appear on the wire). Rather than storing that buffer encoded with
no headroom, requiring re-allocating to add the message length and
two MAC blocks, we here add the headroom prior to pushing it into
the gossip buffer, avoiding an allocation.

Avoid a `tokio::mpsc::Sender` clone for each P2P send operation

Whenever we go to send bytes to a peer, we need to construct a
waker for tokio to call back into if we need to finish sending
later. That waker needs some reference to the peer's read task to
wake it up, hidden behind a single `*const ()`. To do this, we'd
previously simply stored a `Box<tokio::mpsc::Sender>` in that
pointer, which requires a `clone` for each waker construction. This
leads to substantial malloc traffic.

Instead, here, we replace this box with an `Arc`, leaving a single
`tokio::mpsc::Sender` floating around and simply change the
refcounts whenever we construct a new waker, which we can do
without allocations.

Avoid allocating when checking gossip message signatures

When we check gossip message signatures, there's no reason to
serialize out the full gossip message before hashing, and it
generates a lot of allocations during the initial startup when we
fetch the full gossip from peers.

Stop writing signer data as a part of channels

This breaks backwards compatibility with versions of LDK prior to
0.0.113 as they expect to always read signer data.

This also substantially reduces allocations during `ChannelManager`
serialization, as we currently don't pre-allocate the `Vec` that
the signer gets written in to. We could alternatively pre-allocate
that `Vec`, but we've been set up to skip the write entirely for a
while, and 0.0.113 was released nearly a year ago. Users
downgrading to LDK 0.0.112 and before at this point should not be
expected.

Update MuSig2 dependency for Hash trait derivation.

Add Splicing (and Quiescence) wire message definitions

Rely on const generic big arrays for `PartialEq` in msgs

Implementation of standard traits on arrays longer than 32 elements
was shipped in rustc 1.47, which is below our MSRV of 1.48 and we
can use to remove some unnecessary manual implementation of
`PartialEq` on `OnionPacket`.

`derive(Hash)` for P2P messages

In other languages (Java and C#, notably), overriding `Eq` without
overriding `Hash` can lead to surprising or broken behavior. Even
in Rust, its usually the case that you actually want both. Here we
add missing `Hash` derivations for P2P messages, to at least
address the first pile of warnings the C# compiler dumps.

Log the error, when trying to forward the intercepted HTLC, but the

channel is not found

Don't send init `closing_signed` too early after final HTLC removal

If we remove an HTLC (or fee update), commit, and receive our
counterparty's `revoke_and_ack`, we remove all knowledge of said
HTLC (or fee update). However, the latest local commitment
transaction that we can broadcast still contains the HTLC (or old
fee), thus we are not eligible for initiating the `closing_signed`
negotiation if we're shutting down and are generally expecting a
counterparty `commitment_signed` immediately.

Because we don't have any tracking of these updates in the `Channel`
(only the `ChannelMonitor` is aware of the HTLC being in our latest
local commitment transaction), we'd previously send a
`closing_signed` too early, causing LDK<->LDK channels with an HTLC
pending towards the channel initiator at the time of `shutdown` to
always fail to cooperatively close.

To fix this race, we add an additional unpersisted bool to
`Channel` and use that to gate sending the initial `closing_signed`.

Replace maze of BOLT11 payment utilities with parameter generators

`lightning-invoice` was historically responsible for actually
paying invoices, handling retries and everything. However, that
turned out to be buggy and hard to maintain, so the payment logic
was eventually moved into `ChannelManager`. However, the old
utilites remain.

Because our payment logic has a number of tunable parameters and
there are different ways to pay a BOLT11 invoice, we ended up with
six different methods to pay or probe a BOLT11 invoice, with more
requested as various options still were not exposed.

Instead, here, we replace all six methods with two simple ones
which return the arguments which need to be passed to
`ChannelManager`. Those arguments can be further tweaked before
passing them on, allowing more flexibility.

Drop old `expiry_time_from_unix_epoch` helper in expiry time lookup

Since there's a much simpler way to go about it with
`Bolt11Invoice::expires_at`.

Drop non-anchor channel fee upper bound limit entirely

Quite a while ago we added checks for the total current dust
exposure on a channel to explicitly limit dust inflation attacks.
When we did this, we kept the existing upper bound on the channel's
feerate in place. However, these two things are redundant - the
point of the feerate upper bound is to prevent dust inflation, and
it does so in a crude way that can cause spurious force-closures.

Here we simply drop the upper bound entirely, relying on the dust
inflation limit to prevent dust inflation instead.

Impl display for invoice fields

Make invoice fields public

Have Invoice Description use UntrustedString

peel_payment_onion static fn in channelmanager

remove obsolete comment

InboundOnionErr fields public

Bump rust-bitcoin to v0.30.2

Remove nightly warnings

Drop panic if `rust-bitcoin` adds a new `Network`

`rust-bitcoin` 0.30 added `#[non_exhaustive]` to the `Network`
enum, allowing them to "add support" for a new network type without
a major version change in the future. When upgrading, we added a
simple `unreachable` for the general match arm, which would break
in a minor version change of `rust-bitcoin`.

While it seems [possible rust-bitcoin will change
this](rust-bitcoin/rust-bitcoin#2225),
we still shouldn't ba panicking, which we drop here in favor of a
`debug_assert`ion, and a default value.

Remove now-redundant checks in BOLT12 `Invoice` fallback addresses

Now that we use the `rust-bitcoin` `WitnessProgram` to check our
addresses, we can just rely on it, rather than checking the program
length and version.

Explicitly reject routes that double-back

- If a path within a route passes through the same channelID twice,
  that shows the path is looped and will be rejected by nodes.
- Add a check to explicitly reject such payment before trying to send
  them.

Add test for PathParameterError introduced in previous commit

- Also modify the unwrap_send_err!() macro to handle the
  PathParameterError

Return confirmation height via `Confirm::get_relevant_txids`

We previously included the block hash, but it's also useful to include
the height under which we expect the respective transaction to be
confirmed.

Use upstream `TestLogger` util in tx sync tests

Improve `EsploraSyncClient` logging

We give some more information while reducing the log levels to make the
logging less spammy.

We also convert one safe-to-unwrap case from returning an error to
unwrapping the value.

Improve `EsploraSyncClient` test coverage

In particular, we now test `register_output` functionality, too.

Move `sync_` methods to `SyncState`

Set `pending_sync` when last-minute check fails in Esplora

Implement `ElectrumSyncClient`

Add Electrum integration test

DRY up Esplora/Electrum `integration_tests`

Use `esplora-client`'s `async-https-rustls` feature

Now that we upgraded `esplora-client` to 0.6 we can use
`async-https-rustls` instead of manually overriding the `reqwest`
dependency.

Implement struct wrappers for channel key types to avoid confusion.

Currently all channel keys and their basepoints exist uniformly as
`PublicKey` type, which not only makes in harder for a developer to
distinguish those entities, but also does not engage the language
type system to check if the correct key is being used in any
particular function.

Having struct wrappers around keys also enables more nuanced
semantics allowing to express Lightning Protocol rules in language.
For example, the code allows to derive `HtlcKey` from
`HtlcBasepoint` and not from `PaymentBasepoint`.

This change is transparent for channel monitors that will use the
internal public key of a wrapper.

Payment, DelayedPayment, HTLC and Revocation basepoints and their
derived keys are now wrapped into a specific struct that make it
distinguishable for the Rust type system. Functions that require a
specific key or basepoint should not use generic Public Key, but
require a specific key wrapper struct to engage Rust type
verification system and make it more clear for developers which
key is used.

Add channel_keys_id as param in get_destination_script

This enables implementers to generate a different destination script for each channel.

Add `channel_keys_id` to `SpendableOutputDescriptor::StaticOutput`

In 7f0fd86, `channel_keys_id` was
added as an argument to `SignerProvider::get_destination_script`,
allowing implementors to generate a new script for each channel.

This is great, however users then have no way to re-derive the
corresponding private key when they ultimately receive a
`SpendableOutputDescriptor::StaticOutput`. Instead, they have to
track all the addresses as they derive them separately. In many
cases this is fine, but we should support both deployments, which
we do here by simply including the missing `channel_keys_id` for
the user.

Rename SignerProvider's Signer to EcdsaSigner.

Introduce TaprootSigner trait.

For Taproot support, we need to define an alternative trait to
EcdsaChannelSigner. This trait will be implemented by all signers
that wish to support Taproot channels.

Add TaprootSigner variant to SignerProvider.

Previously, SignerProvider was not laid out to support multiple signer
types. However, with the distinction between ECDSA and Taproot signers,
we now need to account for SignerProviders needing to support both.

This approach does mean that if ever we introduced another signer type
in the future, all implementers of SignerProvider would need to add it
as an associated type, and would also need to write a set of dummy
implementations for any Signer trait they do not wish to support.

For the time being, the TaprootSigner associated type is cfg-gated.

Reparametrize ChannelSignerType by SignerProvider.

ChannelSignerType is an enum that contains variants of all currently
supported signer types. Given that those signer types are enumerated
as associated types in multiple places, it is prudent to denote one
type as the authority on signer types.

SignerProvider seemed like the best option. Thus, instead of
ChannelSignerType declaring the associated types itself, it simply
uses their definitions from SignerProvider.

Move ECDSA-specific signers into ecdsa.rs

To separate out the logic in the `sign` module, which will start to be
convoluted with multiple signer types, we're splitting out each signer
type into its own submodule, following the taproot.rs example from a
previous commit.

Gate Taproot-related todos behind cfg flag.

Remove superfluous commitment_number parameter.

Move validate_counterparty_revocation to ChannelSigner.

Remove unused Taproot import.

Fix `data_loss_protect` test to actually test DLP

The data loss protect test was panicking in a message assertion
which should be passing, but because the test was marked only
`#[should_panic]` it was being treated as a successful outcome.
Instead, we use `catch_unwind` on exactly the line we expect to
panic to ensure we are hitting the right one.

Clean up error messages and conditionals in reestablish handling

When we reestablish there are generally always 4 conditions for
both local and remote commitment transactions:
 * we're stale and have possibly lost data
 * we're ahead and the peer has lost data
 * we're caught up
 * we're nearly caught up and need to retransmit one update.

In especially the local commitment case we had a mess of different
comparisons, which is improved here. Further, the error messages
are clarified and include more information.

Handle missing case in reestablish local commitment number checks

If we're behind exactly one commitment (which we've revoked), we'd
previously force-close the channel, guaranteeing we'll lose funds
as the counterparty has our latest local commitment state's
revocation secret.

While this shouldn't happen because users should never lose data,
sometimes issues happen, and we should ensure we always panic.

Further, `test_data_loss_protect` is updated to test this case.

move static channelmanager functions into their own file

Parse blinding point in UpdateAddHTLC

A blinding point is provided in update_add_htlc messages if we are relaying or
receiving a payment within a blinded path, to decrypt the onion routing packet
and the recipient-provided encrypted payload within. Will be used in upcoming
commits.

onion_utils: extract decrypting faiure packet into method

Will be used in the next commit to parse onion errors from blinded paths in
tests only.

Parse blinded onion errors in tests only.

So we can make sure they're encoded properly.

Persist outbound blinding points in Channel

A blinding point is provided in update_add_htlc messages if we are relaying or
receiving a payment within a blinded path, to decrypt the onion routing packet
and the recipient-provided encrypted payload within. Will be used in upcoming
commits.

Store whether a forwarded HTLC is blinded in PendingHTLCRouting

We need to store the inbound blinding point in PendingHTLCRouting in order to
calculate the outbound blinding point.

The new BlindedForward struct will be augmented when we add support for
forwarding as a non-intro node.

Persist whether an HTLC is blinded in HTLCPreviousHopData.

Useful so we know to fail blinded intro node HTLCs back with an
invalid_onion_blinding error per BOLT 4.

Another variant will be added to the new Blinded enum when we support
receiving/forwarding as a non-intro node.

Set HTLCPreviousHopData::blinded on intro node forward.

Useful so we know to fail back blinded HTLCs where we are the intro node with
the invalid_onion_blinding error per BOLT 4.

We don't set this field for blinded received HTLCs because we don't support
receiving to multi-hop blinded paths yet, and there's no point in setting it
for HTLCs received to 1-hop blinded paths because per the spec they should fail
back using an unblinded error code.

Parameterize Channel's htlc forward method by outbound blinding point

Used in the next commit to set the update_add blinding point on HTLC forward.

Set update_add blinding point on HTLC forward

Used by the next hop to decode their blinded onion payload.

Parse blinded forward-as-intro onion payloads

Previously, we only parsed blinded receive payloads.

Support forwarding blinded HTLCs as intro node.

Error handling will be completed in upcoming commits.

Remove now-unused Readable impl for ReceiveTlvs

Test blinded forward failure to calculate outbound cltv expiry

Intro node failure only.

Test blinded forwarding payload encoded as receive error case

Correctly fail back on outbound channel check for blinded HTLC

Forwarding intro nodes should always fail with 0x8000|0x4000|24.

Extract blinded route param creation into test util

Correctly fail back blinded inbound fwd HTLCs when adding to a Channel

As the intro node.

Correctly fail back downstream-failed blinded HTLCs as intro

Test intro node blinded HTLC failing in process_pending_htlc_fwds.

Test intro node failing blinded intercept HTLC.

Add release note for blinded HTLC backwards compat.

Test blinding point serialization in Channel.

adding #
@Kixunil
Copy link
Collaborator

Kixunil commented Dec 12, 2023

NACK, I believe having the enum #[non_exhaustive] makes more sense. As we can already observe, there's wide variety of applications supporting different networks. Electrum supports simnet (IDK what it is) in addition to the networks we support for instance, some applications do not support the existing networks and others are only defined for the current ones and it's unclear what will happen when there's a new one. For instance currency in LN is defined for each network we have today except for simnet. It's not predetermined what will happen once this changes.

As such I believe the parameter is application-specific and cannot be universal unless the application is specifically built to support all possible networks. (This is possible, I did this for Firefish by using magic as the network identifier when serialized and native parsing methods for stringly inputs.) And if an application is not made universal then it has to have its own type to represent the network it supports, with conversions and such.

I'm aware this is more annoying but like many other things in Rust, it prevents problems by dealing with these things upfront. I'm still interested in making the API easier. For instance I think using impl Into<Network> in function signatures may be a good idea, just must be weighted against loss of inference.

Another approach we could take for selected applications is to add methods that convert values to and from their representation. E.g. we could add methods to return LN currency string. But the conditions for inclusion would be very strict: we would have to be convinced that whenever Core releases support for a new network the appropriate teams will define how this is mapped to their domain ASAP. So e.g. if Core adds foonet then LN folks will quickly decide what the string for currency is. FTR I currently don't know of any case where I would be confident to do that.

We could also provide a macro that makes it easier to define your own network types if it helps.

@tnull
Copy link
Contributor

tnull commented Dec 12, 2023

As such I believe the parameter is application-specific and cannot be universal unless the application is specifically built to support all possible networks. (This is possible, I did this for Firefish by using magic as the network identifier when serialized and native parsing methods for stringly inputs.) And if an application is not made universal then it has to have its own type to represent the network it supports, with conversions and such.

I'm aware this is more annoying but like many other things in Rust, it prevents problems by dealing with these things upfront. I'm still interested in making the API easier. For instance I think using impl Into<Network> in function signatures may be a good idea, just must be weighted against loss of inference.

IIUC, you're suggesting that dependent crates should be kept from using the Network type in their API and instead should offer their own types and utilities allowing to parse and set the networks they support. I'm not quite sure why this would be preferable to having a Rust type that users can rely on. Wouldn't each crate defining their own APIs suffer from the same core issue (not all supporting all networks), but just shift the resolution to their respective string parsing logic? I.e., we'd trade a strong type for conventions around the bitcoin/testnet/regtest etc string representations?

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 12, 2023

They already suffer the issue, they just wouldn't need to convert between networks inside business rules but at the boundary which is good. I don't see how it's related to strings, since the crates can (and should) define their own strong types.

Also if there's a collection of crates that is guaranteed to support some specific set of networks they should share a type.

@TheBlueMatt
Copy link
Member Author

I'm aware this is more annoying but like many other things in Rust, it prevents problems by dealing with these things upfront.

That's not accurate - it doesn't force users to deal with a new network up front, it forces users to deal with an unknown network up front. If we remove #[non_exhaustive] that's not a case that ever exists, a change to rust-bitcoin to add a new network will simply require handling a new network, never an unknown network.

@TheBlueMatt
Copy link
Member Author

Another way to look at rust-bitcoin's Network is "the minimum set of supported networks that everyone should really support", which I think makes perfect sense and aligns with what you describe, and also definitely doesn't need #[non_exhaustive].

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 12, 2023

it forces users to deal with an unknown network up front

It doesn't force users to do that. It's entirely possible to design an application that uses no match and communicates with external entities using already-standard types/interfaces (magic, human-readable names). I know this because I've done it in an actual production application. Also IIRC elects is written this way too.

The fact that you perceive it as forcing stems from you happening to work on a library where the protocol itself forces you to do something different. And because you're forced to use a specific set of networks you should define your own - which you did with the Currency type. If you impl From<Currency> for Network then you nearly won't see a big difference. Calling into() sometimes is fine, we can remove it completely trading it for worse inference.

For the opposite conversion, I have verified that we never return Network from any function. (LMK if I missed something.) It's always Option<Network> or Result<Network> this means that for any case where you'd be forced to deal with an unknown network you would be regardless! You can just map whatever you don't support to None/Err:

let currency = match Network::from_chain_hash(chain_hash) {
    Some(Network::BItcoin) => Currency::Bitcoin,
    Some(Network::Regtest) => Currency::Regtest,
    Some(Network::Testnet) => Currency::Testnet,
    Some(Network::Signet) => Currency::Signet,
    _ => return Err(UnknownChainHash(chain_hash)),
}

The code is pretty elegant IMO.

If we remove #[non_exhaustive] that's not a case that ever exists, a change to rust-bitcoin to add a new network will simply require handling a new network

It's an unknown network for the protocol that doesn't define it. But what's worse it will force breakage across the entire ecosystem and cause coordination nightmares whenever a new network is added. We're trying to bring the crate to the state where each crate can rely on 1.0 primitives and never worry about breakages again. Isn't it worth a few more lines in your code?

Another way to look at rust-bitcoin's Network is "the minimum set of supported networks that everyone should really support",

I disagree with this view in two ways. First, I personally see very little value in supporting anything else than mainnet and regtest for most applications. Regtest is million times easier to work with than everything else when it comes to testing. So I don't understand why the type should have other variants if this is its definition. Second, the type really is "whatever Core supports" this can be clearly seen from having to_core_arg (infallible) and from_core_arg which suggests that we've committed to never adding a network that Core doesn't support. The other conversions are also defined in Core (but it's consensus, so it doesn't matter much).

@TheBlueMatt
Copy link
Member Author

The fact that you perceive it as forcing stems from you happening to work on a library where the protocol itself forces you to do something different. And because you're forced to use a specific set of networks you should define your own - which you did with the Currency type. If you impl From for Network then you nearly won't see a big difference. Calling into() sometimes is fine, we can remove it completely trading it for worse inference.

As you note, this is incredibly common. Sadly Bitcoin is filled with people defining their own network enum. Ensuring convertibility between them is a critical part of the Network API, and not having good convertibility between them would be a massive PITA for everyone (I think on this point we agree). I don't see why "you cannot support From<Network>" is a reasonable position, tbh. Users are going to use the stuff that's exposed, and not always in the idealized way that you have in your head, but rather usually use things in the way that's obvious, which I'd say the LDK code here is.

Network isn't just "a type for rust-bitcoin to communicate the network that some deserialized thing used", its also a general type. Tons of downstream applications want to use it as their network type, and support the same set of networks as Bitcoin Core and rust-bitcoin. I don't see why we need to break those application.

For example, given the BOLT 11 set is congruous with the rust-bitcoin set, I'd like to drop the lightning-invoice Currency type (it only relatively recently took rust-bitcoin as a dependency at all). Now we can't do a common operation, like serialize the object. We store a Network in some Invoice, having checked it in any deserialization function (or simply taken one from the user) and now we get a call to serialize...what do we do? We have to write some magic bytes corresponding to the Network, and we know we've handled all the ones that are possible, but now we're force do handle some unknown network. Obviously if rust-bitcoin adds a new network its not hard to define some new magic bytes, but that's not the issue, we have to handle unknown networks.

Second, the type really is "whatever Core supports" this can be clearly seen from having to_core_arg (infallible) and from_core_arg which suggests that we've committed to never adding a network that Core doesn't support. The other conversions are also defined in Core (but it's consensus, so it doesn't matter much).

Fair enough, I'd be fine with that too. But that also doesn't need #[non_exhaustive].

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 12, 2023

Sadly Bitcoin is filled with people defining their own network enum.

Really? Can you point to specific examples? I haven't seen any other than Currency in lightning which is perfectly reasonable.

not having good convertibility between them would be a massive PITA for everyone (I think on this point we agree)

It already is by simply the applications and, more importantly, protocols not being designed to be network-agnostic. You can't change it by making the enum not #[non_exhaustive] because then we'll need to issue breaking releases and you can't convert between different versions of Network so we're back at square one and even worse - it infects all other types in the crate so the whole crate becomes incompatible!

Users are going to use the stuff that's exposed, and not always in the idealized way

I'm willing to accept it for many other things. But after fighting so hard to get this crate to stabilize one day being told that's not going to happen is absolutely horrible to hear. If that's the position of other maintainers I might be better off forking the crate.

given the BOLT 11 set is congruous with the rust-bitcoin set

Only today. It's perfectly possible it won't be in the future. E.g. I have an example of a network that I believe could be interesting: signed like signet but zero difficulty like regtest. And when that happens we get ecosystem-wide breakage forcing everyone to update or be incompatible.

We store a Network in some Invoice

Yeah, that's the incorrect thing. You should store Currency. Basically use Currency everywhere except when communicating with the bitcoin crate. Then it works fine. This is the classic Rust pattern that you validate data at boundary and use strong type inside so I'm not even proposing anything exotic.

@TheBlueMatt
Copy link
Member Author

I'm willing to accept it for many other things. But after fighting so hard to get this crate to stabilize one day being told that's not going to happen is absolutely horrible to hear. If that's the position of other maintainers I might be better off forking the crate.

I'm really confused why #[non_exhaustive] on Network is required to stabilize rust-bitcoin. The Network enum (in Bitcoin Core) has had one new variant added in the last decade or so. Suggesting its going to be a common enough occurrence that we need to make the crate harder to use just to be able to add new networks all the time seems like missing the forest for one tree.

Only today. It's perfectly possible it won't be in the future. E.g. I have an example of a network that I believe could be interesting: signed like signet but zero difficulty like regtest. And when that happens we get ecosystem-wide breakage forcing everyone to update or be incompatible.

If that becomes commonly used enough that it merits inclusion in Bitcoin Core and rust-bitcoin then it probably is!

Yeah, that's the incorrect thing. You should store Currency. Basically use Currency everywhere except when communicating with the bitcoin crate. Then it works fine. This is the classic Rust pattern that you validate data at boundary and use strong type inside so I'm not even proposing anything exotic.

The whole point of that paragraph was me pointing out that it would be totally sensible to drop Currency, so in that world it wouldnt exist :)

@TheBlueMatt
Copy link
Member Author

One note about the desire to drop Currency - rust-bitcoin is basically the common, ecosystem-wide "core set of types" crate. That means, its Network enum is the common, ecosystem-wide Network enum across BDK, LDK, fedimint, etc, etc. We shouldn't be considering it in some kind of myopic isolation.

@tcharding
Copy link
Member

Can this debate be reduced to the following question:

  • rust-bitcoin v1.0.0 is released.
  • Bitcoin Core adds a new network.
  • Should users of rust-bitcoin be able to use v1.1.0 to get the new network or is it reasonable to force them to use v2.0.0 to get it (forcing them to take all the other changes in v2.0)?

Its a trade off, right? Upgrade to v2.0.0 might be super painful (or timewise impossible) but enabling usage of v1.1.0 is painful because it requires non_exhaustive on the Network type.

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 12, 2023

@TheBlueMatt I took a look at LDK and I found that nothing is broken, just From needed to be changed to TryFrom so it looks like it's not as big of a deal as you thought? Did I miss something? See this PR: lightningdevkit/rust-lightning#2789

I've searched for other instances of matching on Network and didn't find any. So it seems your crate is completely unaffected aside from Currency being a separate type which may be annoying to some people.

@tcharding yes, that summary is accurate, though maybe #[non_ehaustive] is not that painful. I've yet to see a crate where it causes a massive breakage.

@TheBlueMatt
Copy link
Member Author

I think that's generally the case, yes. I think its totally reasonable to expect some amount of pain to use a whole new network, because it won't just impact the Network enum, but is also likely to impact other things, like how work calculations work across blocks, what difficulty means, etc. Supporting a new network will never be as easy as a small dependency bump and a new enum variant. Maybe we can do it all without an actual semver breaking change, but I'm not sure if that really should be a goal at all, doubly so given just how infrequently new networks are added.

@TheBlueMatt
Copy link
Member Author

I found that nothing is broken, just From needed to be changed to TryFrom so it looks like it's not as big of a deal as you thought?

Great, so now we've pushed the dead-code error handling from rust-bitcoin down into LDK and then pushed it further into the code of our users, rather than just not having to handle the non-case at all :)

@TheBlueMatt
Copy link
Member Author

I think there's one secondary concern beyond just "should supporting a new network require a new rust-bitcoin release" - in my previous comment I noted that

One note about the desire to drop Currency - rust-bitcoin is basically the common, ecosystem-wide "core set of types" crate. That means, its Network enum is the common, ecosystem-wide Network enum across BDK, LDK, fedimint, etc, etc. We shouldn't be considering it in some kind of myopic isolation.

If we think this is a reasonable use of the Network enum then istm a new variant should be a major release, irrespective of semver concerns, because it implies downstream libraries (though not really applications) need to handle the new network.

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 12, 2023

but is also likely to impact other things, like how work calculations work across blocks, what difficulty means, etc

Those are entirely contained within Params and our API methods and will be even more when we implement proper difficulty adjustment. Moreover, many, many applications don't really need to deal with these things as they simply connect to Core and accept it as the source of truth.

Great, so now we've pushed the dead-code error handling from rust-bitcoin down into LDK and then pushed it further into the code of our users, rather than just not having to handle the non-case at all :)

They already have to handle invalid inputs when they parse and if they use Network instead they only have to change it to Currency.

If we think this is a reasonable use of the Network enum then istm a new variant should be a major release, irrespective of semver concerns, because it implies downstream libraries (though not really applications) need to handle the new network.

I don't think so. Either the library is written to work with any network in which case they can be updated with no code change at all or the library for some reason cannot support other networks in which case it will just return errors for the unknown ones and the applications using them will propagate those errors. Nothing really has to be broken.

I was thinking of putting the network into a separate crate (with whatever other things are likely to break for stupid reasons like someone else releasing something) and then make the crate optional everywhere but I don't think it works since it forces Address to release a new major version anyway which still breaks things.

Anyway, since you're annoyed about Currency and it's only related to parsing or serializing BOLT11 invoices maybe it'd help you if we just added the methods to the Network. However to accept it I'd want to see several LN protocol developers to agree that in the event of a new network added in Core they will either define what the new values are within one month of Core release or leave it up to rust-bitcoin developers to decide if they can't in time. :)

@apoelstra
Copy link
Member

No library can be written to handle "any hypothetical future network" but it's easy in most cases to write a library that supports all the existing networks. I agree with Matt that adding new networks is (a) extremely rare, and (b) likely to break things beyond the enum, even if those are things that 99% of users will never touch.

So by making the enum #[non_exhaustive] and forcing people to use TryFrom for conversions that are actually infallible, we're just creating dead error paths that are untestable until an undefined future date.

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 13, 2023

No library can be written to handle "any hypothetical future network"

Funny that you say this because I wrote such library. :) It is perfectly possible if there's no silly protocol that would invent its own representations for networks. In my case I encode Network by converting it to Magic and back and on the side of WASM consumers I just take and parse strings because JavaScript can't reasonably use anything else anyway.

I think we can decouple Network at least from units and blockdata (or just transaction) If we do that then I'm more open to it since that would minimize the damage. That being said, Address is also a significant API piece that happens to use Network - theoretically, we're now moving away from it and since we're using impl Trait in arguments we might be able to separate that one as well.

Still it sucks that any software that did this right will get punished because majority of stuff is badly designed.

@apoelstra
Copy link
Member

It is perfectly possible if there's no silly protocol that would invent its own representations for networks. In my case I encode Network by converting it to Magic and back and on the side of WASM consumers I just take and parse strings because JavaScript can't reasonably use anything else anyway.

This sounds like you throw away the type information from Network, then getting it back is necessarily fallible, giving you an error path that you can use to handle unknown networks. But if somebody wasn't round-tripping to magics or strings, they could write code without error paths. Unless the Network enum were tagged #[non_exhaustive], that is. Then they would need error paths that were impossible to execute until a probably-never-coming version of rust-bitcoin finally adds a new variant.

@Kixunil
Copy link
Collaborator

Kixunil commented Dec 14, 2023

then getting it back is necessarily fallible

That's only done during parsing which is already fallible so it doesn't matter (and the compiler will easily remove the dead branch).

But if somebody wasn't round-tripping to magics or strings, they could write code without error paths.

Is there an actual real code that needs that though? Even LDK, a super-big crate, didn't have a single line that needed to be changed apart From -> TryFrom. And what are people doing with Network if not eventually translating it to something else?

@TheBlueMatt
Copy link
Member Author

Apologies, I stepped away from this issue because I was feeling incredibly frustrated, and then got busy with other things and eventually holidays.

Funny that you say this because I wrote such library. :) It is perfectly possible if there's no silly protocol that would invent its own representations for networks.

This is an oversimplification. Sure, if you're only doing very trivial things maybe, but what happens if we add a BitcoinInquisition Network and suddenly there's different consensus rules at the script level? If you only use high level APIs in rust-bitcoin to interact with the system maybe we can make it work, but nontrivial projects (like LDK, BDK, etc) probably do much lower-level things that are going to be impacted by consensus changes. Hell, even LDK cares about things like rough concepts of if a block may be valid, which may or may not be entirely expressible with rust-bitcoin (given the concept of validity is somewhat out of scope).

Is there an actual real code that needs that though? Even LDK, a super-big crate, didn't have a single line that needed to be changed apart From -> TryFrom. And what are people doing with Network if not eventually translating it to something else?

Yes, the LDK multi-language bindings expose a concept of Network to C/Java/Swift/TypeScript, all of which do not have the ability to have some kind of #[non_exhaustive], and as a result need an unreachable!() to handle the unknown case. If we only care about Rust, there's also the other case I mentioned - in LDK we really should drop the stupid lightning-invoice Currency type and use the rust-bitcoin Network type, since the current set of networks is the same forcing users to deal with conversion is just extra work that they don't need. Making it #[non_exhaustive] makes this difficult.

@apoelstra
Copy link
Member

libcpocalypse happened precisely because libc updated something that was rarely used. If people need to upgrade fundamental crates to incompatible versions we have libcpocalypse.

They don't. Applications that want to support the new network need to switch from Network to Network2. To the extent Network2 supports all the Into impls that the old one did, everything will just work. And if Network2 doesn't support these, that reflects an actual breaking change associated with the new network, and the application will need to switch to a new API (which plausibly could mean switching to another ecosystem-wide fork but I'm doubtful anything would be that extreme; it seems we can always add new APIs in parallel).

Libraries meanwhile can continue to use whatever combination of Into impls and concrete types reflects the networks they support. And if they use the Into impls the type system will ensure they're not lying, unlike wildcard matches which are a runtime promise that "whatever this network is, it'll work with my library".

@Kixunil
Copy link
Collaborator

Kixunil commented Jan 26, 2024

We've now had several cases of downstream users complaining that #[non_exhaustive] broke their intended use of the Network enum in several ways.

Please forward those complaints to us. Just knowing the number of them and not their kind or "quality" isn't very helpful.

Its important that there exist some enum somewhere that we can use across BDK/LDK/fedi/rust-bitcoin/etc.

That can only happen if the LN developers give up their authority to define HRP for invoices to the rust-bitcoin developers. Let's say they are lazy/forgetful just like whoever didn't assign a special signet HRP for bech32, what will you do then? There logically cannot be one enum. At best there can be two: one for LN - Currency, one for bitcoin.

If we want that to be in a separate crate that rust-bitcoin depends on with some Into, fine

OK, let's try to progress on this then. I'll try to split it up after primitives are separated if nobody beats me to it.

there's a whole ecosystem out there, and we need to consider how best to integrate across them

Well, the funny part is if they all have the same enum they just trade immediate benefit for significant breakages later. And it's likely they don't realize it. It's the same old story about Rust: some API looks too complex and later you realize it's because of some reason you didn't even know about and it actually makes sense. And then people say things like Rust is difficult or complain at various forums where more experienced people have to explain to them that if they don't want the safety/security benefits provided by Rust maybe they should use something else... (Not trying to shame anyone here, just try to look at things from Rust perspective. I understand it takes time.)

@TheBlueMatt
Copy link
Member Author

Please forward those complaints to us. Just knowing the number of them and not their kind or "quality" isn't very helpful.

I believe they're all in this thread, both the other issue and the several instances of LDK preferring to use Network (language bindings needs it, but also we'd like to drop Currency in lightning-invoice).

That can only happen if the LN developers give up their authority to define HRP for invoices to the rust-bitcoin developers. Let's say they are lazy/forgetful just like whoever didn't assign a special signet HRP for bech32, what will you do then? There logically cannot be one enum. At best there can be two: one for LN - Currency, one for bitcoin.

Oh come on. Back in the real world, there are a very small number of Networks and almost certainly any one(s) that rust-bitcoin supports, lightning-invoice will too.

Well, the funny part is if they all have the same enum they just trade immediate benefit for significant breakages later.

No, they don't, this assumes some future where rust-bitcoin starts supporting a new network and all the other crates upgrade, but also don't want to support the new network and somehow aren't able to invest the time to create a new enum at that juncture. Please let's stop making up fanciful scenarios of how new networks may come to be that just aren't realistic and not design our API around it.

@apoelstra
Copy link
Member

Please forward those complaints to us. Just knowing the number of them and not their kind or "quality" isn't very helpful.

I'm really struggling to understand this point of view and I've read this entire thread. If the enum is #[non_exhaustive] then it's impossible to match on it and anybody who needs to match on it is forced to go make their own enum. (Or they can stick untestable paths with unknowable semantics into their code which will become active in the future without warning.)

It seems like if we want to imagine that new future networks will match existing networks in significant ways (e.g. using bech32m but with different HRPs, using the same network protocol but with different constants) then we can make our imagining explicit by using Into<Hrp> and the like. In this way libraries can be generic and future-proof without eliminating a "standard set of networks enum" that can be shared amongst multiple applications and their associated libraries.

@Kixunil
Copy link
Collaborator

Kixunil commented Jan 27, 2024

I believe they're all in this thread, both the other issue and the several instances of LDK preferring to use Network (language bindings needs it, but also we'd like to drop Currency in lightning-invoice).

I mean, I would like to see specific code and more examples of it. What I want to know is if all of them are "I can't be bothered to write my own" or if there are more serious issues. It's the exact thing you pointed out: I see mainly rust-bitcoin and not that much of wider ecosystem.

and almost certainly any one(s) that rust-bitcoin supports, lightning-invoice will too.

In real world we have a ton of incoherent garbage because someone didn't specify legacy address prefix when coming up with regtest, then someone didn't specify HRP when coming up with signet, then I remember hearing the term simnet which isn't even in Core and now I'm supposed to believe you that lightning developers are the bright exception that will swiftly standardize a unique HRP for any new network that is added to Core? Even though there are multiple teams that already have hard time agreeing on things?

I'd love to see some track record that would prove otherwise and I'd absolutely love not having 3+ different enums in the codebase but if this is the reality, rather than hiding from it and hitting problems down the line I want it represented in the type system today so I can be confident it won't break horribly in the future.

No, they don't, this assumes some future where rust-bitcoin starts supporting a new network and all the other crates upgrade,

It doesn't need to assume the latter because if the enum is exhaustive then a single crate upgrading will break everything unless every single upstream crate wants to backport everything just to keep the old Network alive. I've seen an old codebase where lot of backporting and otherwise duplicating things led to crazy inconsistencies. Absolutely wouldn't recommend.

And even if we did this, the history says that most likely there will be some ridiculous edge case like unspecified HRP and people will start using an existing one and then things stop roundtripping and you'll be pulling your hair out trying to clean up the mess.

I'm really struggling to understand this point of view

I know the theoretical problems with #[non_exhaustive] but I've seen zero lines of code where this is a practical problem. I want to see it, so I ask for examples. I do want to help but not over-promise in API just because someone is too lazy to write a few lines to define an enum.

unknowable semantics

Seems pretty clear to me: only changes to how blocks headers are validated and only soft-forks when it comes to other changes. (Note that every app already must assume soft forks.) But surely, we should document that. Also even if it's untestable, anything that's not mainnet is not a big deal and mainnet is already defined.

then we can make our imagining explicit by using Into<Hrp> and the like

Yes, I want that.

libraries can be generic and future-proof

Yes, unfortunately, nothing encourages them to be - we wouldn't have this discussion otherwise. But whatever, that's on them. If bitcoin is solid, then I'll just use the good stuff for my projects.

@apoelstra
Copy link
Member

I mean, I would like to see specific code and more examples of it.

From some test code within Liquid, setting up a bitcoin.conf file section for a specific network

        match self.network {
            Some(bitcoin::Network::Bitcoin) | None => {}, 
            Some(bitcoin::Network::Testnet) => {
                writeln!(w, "testnet=1")?;
                if version > 17_00_00 {
                    writeln!(w, "[testnet]")?;
                }   
            }   
            Some(bitcoin::Network::Signet) => {
                writeln!(w, "signet=1")?;
                if version > 17_00_00 {
                    writeln!(w, "[signet]")?;
                }   
            }   
            Some(bitcoin::Network::Regtest) => {
                writeln!(w, "regtest=1")?;
                if version > 17_00_00 {
                    writeln!(w, "[regtest]")?;
                }   
            }   
        }   

What should we do here in case of "unknown network"? (This is test-harness code so on some level it doesn't matter, but it's easy to imagine something like this being used in production.)

Seems pretty clear to me: only changes to how blocks headers are validated and only soft-forks when it comes to other changes.

So, we'd never have a test network with confidential transactions, say, or which replaced all the crypto with post-quantum equivalents, or more generally which tried some big change without bothering to define and test a soft-fork transition mechanism?

You may be correct. But it's not a promise we can make from the rust-bitcoin API.

@Kixunil
Copy link
Collaborator

Kixunil commented Jan 31, 2024

Why not use to_core_arg to produce the correct string?

@apoelstra
Copy link
Member

In this case that would happen to work, because Bitcoin is the only one that's a special case and it already exists. But the fact that there is a special case suggests that we'd rather make this code explicit rather than coercing the output of a related function.

And any future network would still need to be slightly different, because at the very least there would be no pre-0.17 syntax to generate and we'd presumably need to panic or something there.

So basically, yes, we could, but we'd still need to make assumptions about the future network, and the comments describing these assumptions would be longer and less reliable than the existing code.

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 1, 2024

If a new network is added you'll have to "panic or something" on older versions anyway so you might a s well write the code now. But I don't see a versions check for the network in the code you posted so either it's a bug in your code or you intentionally don't care about it. But also, this just gave me an idea that we should have a method returning the version Core added the network in. (Regardless of your use case.)

If you think making these assumptions is unacceptable for you then you really should make your own enum for networks which will give you option to upgrade whenever you need it rather than being forced to either change the code or miss new features in bitcoin. Though now I realize a common versioned Network is great because people who share the version don't have to write effectively the same enum multiple times.

@apoelstra
Copy link
Member

But I don't see a versions check for the network in the code you posted so either it's a bug in your code or you intentionally don't care about it.

A version check for what network?

Though now I realize a common versioned Network is great because people who share the version don't have to write effectively the same enum multiple times.

Yes. If we were using Into<Hrp> etc in this library then the Network enum literally wouldn't matter to rust-bitcoin because we'd never be directly using it here. Its only purpose is to provide a canonical "these are the networks that the rust-bitcoin ecosystem supports" indicator and to provide a schelling point for other developers to use as a set of supported networks.

This is why I continue not to see any "libcopalypse" scenario happening, nor do I see a problem with just adding a Network2 enum if we want to widen the set of networks.

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 2, 2024

A version check for what network?

Something like fn is_supported_by_core_version(self, core_version: u32) -> bool

This is why I continue not to see any "libcopalypse" scenario happening,

It's less of a problem if we do it right and also less of a problem for consumers of downstream crates that do it right. I'm not hyped enough to say it's non-issue in that case but it might be.

@apoelstra
Copy link
Member

Core is never going to update so that existing configuration files stop working. So I'm not sure what a Core version check would accomplish.

It's less of a problem if we do it right

If "do it right" means putting #[non_exhaustive] on it, then nobody will use the Network type included in rust-bitcoin so we might as well just drop it.

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 3, 2024

So I'm not sure what a Core version check would accomplish.

Very similar thing your version > 17_00_00 check does: raise a readable error if the core version doesn't support specific network. (In your case you don't need to error.)

If "do it right" means putting #[non_exhaustive] on it,

No, it's doing all these traits and making sure nothing returns it etc...

@apoelstra
Copy link
Member

apoelstra commented Feb 3, 2024

No, it's doing all these traits and making sure nothing returns it etc...

Ok, perfect, we are in agreement there :).

So if we do things right in rust-bitcoin, the Network enum actually will be unsused within rust-bitcoin, other than abstractly as a "Into<Params>" or "Into<Hrp>" or whatever. This gives us a lot of freedom here. We can

  • Have no blessed Network enum at all (personally I could live with this; for my own projects I never need the enum to be interoperable; it sounds like the ldk people would need to define their own enum but they already have a near-universal rust-lightning dep so they could put it there).
  • Have a #[non_exhaustive] Network enum whose documentation explains how to match on it (and we will have a drawn-out fight over what this text should be).
  • Have an exhaustive enum which we will version by releasing rust-bitcoin 2.0 whenever a new network arrives. (I think we all agree this is a bad idea; it would definitely cause libcopalypse).
  • Have an exhaustive enum in its own crate, which gets upgraded by major versions. We advise all libraries to never return the type, accept it only via generics, to access the type through rust-bitcoin, and failing that, accept all published major versions. rust-bitcoin naturally would do all of these things.
  • Have an exhaustive enum which we will version by just adding a parallel `Network2 enum. Again, tell libraries to avoid directly using the type if at all possible.

In fact, we could do any combination of these since none of them actually interfere with each other (though it would probably be confusing!).

My personal view at this point is

  • if we're telling libraries to basically not use the enum, then exhaustiveness becomes irrelevant for libraries (so no risk of libcopalypse) but remains relevant for applications (who want exhaustiveness so that their users aren't suddenly using a new network that the application authors don't know about and can't predict behavior of)
  • having said that , there are some "applications" (e.g. the Lightning Network) which are spread across multiple libraries and actual applications, and they actually will use the enum despite our advice (and I get why that makes you nervous @Kixunil since then our type is implicitly reflecting some LN thing that we have no relationship to). I think this is still fine; if we later add an enum that they don't like, or upgrade our type in a breaking way, they can have a "local" libcopalypse but it won't affect people outside their space

I continue to think we should have an exhaustive enum, with a bunch of documentation about how to use it properly. I am warming up to the "don't have an enum, just have rules for defining your own" but I think this would be needless work for people who just want to grab an off-the-shelf set of networks or people who want to make their own enum but want to see an example of how to do it first.

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 3, 2024

Yeah, pretty much it. I want combination of having #[non_exhaustive] enum in some reasonable crate and an exhaustive one in a separate versioned crate. With all the docs you mention and the enums mentioning each-other.

I think this would be needless work for people who just want to grab an off-the-shelf set of networks or people who want to make their own enum but want to see an example of how to do it first.

Yep.

@tcharding
Copy link
Member

tcharding commented Feb 4, 2024

Cool, if I'm following, we have rough consensus here now, and a solution on how to progress. We can get this done before next release, right?

@TheBlueMatt are you in agreement here or have we missed something? If we haven't I'll get to work.

This still leaves v31 as an annoying release for users though. Can we resolve that, remove non_exhaustive in a point release? If we yank 31.0 and 31.1 and release it as 31.2 we are still semver compliant, right? Is that too drastic?

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 4, 2024

We can get this done before next release, right?

I doubt so, I suspect we need to progress with crate splitting more. But if you find a way to do it I'm fine with it.

Can we resolve that, remove non_exhaustive in a point release?

And we'll be putting it back in the next one and then people will have to fix their code again. It's not really worth it.

@tcharding
Copy link
Member

Fair and fair. I'll just do nothing then :)

@apoelstra
Copy link
Member

What would the crate dep tree look like here? Would we have two separate crates and rust-bitcoin depends on both of them? Where does Params live and where does the Into<Params> impl live?

@Kixunil
Copy link
Collaborator

Kixunil commented Feb 4, 2024

Currently I'm not sure if the non_exhaustive enum should go to a separate crate or it fits somewhere else, I'd like to first split up other things. I also suspect bitcoin can't depend on exhaustive one but the exhaustive one can depend on bitcoin. Params will probably live alongside with the non_exhaustive one.

I think we'll see it more clearly when we're closer. There's other stuff to do now.

@apoelstra
Copy link
Member

Ok, so crate(s) that depend on rust-bitcoin, rather than the other way around. Ok, makes sense.

I suspect both the non-exhaustive and the exhaustive one could go into the same crate -- aside from naming conflicts. Because the non_exhaustive type can be expanded in minor versions, and when the exhaustive type forces major versions, we could semver-trick the non-exhaustive one to prevent breakage. Maaaybe we could even use the same type but have exhaustiveness be feature-gated (this would be a super weird feature-gate that'd turn off the semver trick, so maybe it just doesn't work).

Anyway agreed that there are higher priorities right now and happy to revisit when we're closer.

@tcharding tcharding added this to the 0.32.0 milestone Feb 5, 2024
@Kixunil
Copy link
Collaborator

Kixunil commented Feb 5, 2024

both the non-exhaustive and the exhaustive one could go into the same crate

They can't because they have to be versioned differently. Semver trick would have to maintain old versions indefinitely and cause crate duplication which would trigger CI checks and people would then have to add exceptions. Semver trick is great but not having to use it in the first place is better.

exhaustiveness be feature-gated

Strictly speaking, removing non_exhaustive is not a breaking change but triggers a bunch of warnings about unreachable branches so I'd rather avoid that.

@tcharding
Copy link
Member

Removing the milestone from this issue, we have #2541 to hopefully alleviate the current pain.

@tcharding tcharding removed this from the 0.32.0 milestone Mar 14, 2024
@TheBlueMatt
Copy link
Member Author

@TheBlueMatt are you in agreement here or have we missed something? If we haven't I'll get to work.

Sure, I'm entirely burnt out on this issue and don't really have the bandwidth to argue more. The suggested workaround makes sense to get to to me. Params, by itself, doesn't get us there, but if we get there eventually I'm happy.

apoelstra added a commit that referenced this issue Apr 3, 2024
f6467ac Minimize usage of Network in public API (Tobin C. Harding)
3ec5eff Add Magic::from_params (Tobin C. Harding)

Pull request description:

  Minimize usage of the `Network` enum in the public API.

  See #2225 for context, and #1291 (comment) for an interpretation of that long discussion.

  Close: #2169

ACKs for top commit:
  sanket1729:
    reACK f6467ac.
  apoelstra:
    ACK f6467ac

Tree-SHA512: f12ecd9578371b3162382a9181f7f982e4d0661915af3cfdc21516192cc4abb745e1ff452649a0862445e91232f74287f98eb7e9fc68ed1581ff1a97b7216b6a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants