Skip to content

Integrate Libp2p Network Stack - #828

Merged
aszepieniec merged 109 commits into
masterfrom
asz/libp2p
Jan 28, 2026
Merged

aszepieniec merged 109 commits into
masterfrom
asz/libp2p

Conversation

@aszepieniec

@aszepieniec aszepieniec commented Jan 2, 2026 •

Copy link
Copy Markdown
Contributor

Integrate libp2p Stack

1. The Core Philosophy: "The Bridge Pattern"

The main goal of this PR is to integrate libp2p without discarding our existing, battle-tested consensus logic. Instead of rewriting the peer loops, we use a bridge:

  • Protocol Agnosticism: The PeerLoopHandler::run and its run_wrapper were already generic over any AsyncRead + AsyncWrite stream. Thanks!
  • The Handoff: Whether a connection comes from the legacy TCP stack or a modern libp2p QUIC stream, it is "primed" with a handshake and then handed off to the same loop logic. The loop itself doesn't know (or care) about the underlying transport.

2. Anatomy of the NetworkActor

The NetworkActor is the new "brain" of the libp2p networking layer.

  • State Ownership: It owns the NetworkStack (the aggregated libp2p behaviors) and the Swarm. You can think of the Swarm as a handle on the rest of the internet, or at least the part of the internet that is relevant for the protocol.
  • Lifecycle Management: The NetworkActor is responsible for reacting to events bubbled of from its stack, including in particular StreamGateway events. When a successful StreamGateway event comes in, the NetworkActor spawns the actual Peer Loop task.
  • Context: It holds all the necessary mpsc handles and state references needed to initialize a PeerLoop, acting as the glue between libp2p events and the rest of neptune-core.

3. Custom Subprotocol: StreamGateway

This PR introduces the StreamGateway protocol, as a (relatively) stand-alone protocol on equal footing with libp2p's DCUtR or Kademlia. Its responsibilities are:

  1. Negotiate a new substream via libp2p.
  2. Perform the Neptune-Cash-specific handshake using Bincode.
  3. "Hijack" the stream by pulling it out of the libp2p state machine and passing it to a Peer Loop.

4. Identity & Addressing

We are moving away from SocketAddr toward cryptographic identities:

  • PeerId: Derived from a node's public key (stored in identity.key). This provides stable identity for reputation.
  • Multiaddr: Enables support for multiple transports (TCP, QUIC, Relays) in a single address format.
  • Identity Persistence: Added CLI flags (--incognito, --new-identity, --identity-file) to manage how these cryptographic keys are handled.
  • IP-based banlist: Peers are still banned based on their IP not based on their PeerId; no change here.

5. Connectivity Stack & NAT Traversal

The PR integrates several standard libp2p protocols to solve connectivity issues that the legacy stack struggled with:

  • AutoNAT & UPnP: For self-diagnosis and automatic port forwarding.
  • Relay (v2) & DCUtR: Allows nodes behind restrictive firewalls to remain reachable via proxied connections and then "hole punch" to a direct connection.
  • Kademlia (K-DHT): Handles peer discovery. Note: The routing table is currently in-memory; we rely on the persisted Address Book to seed the DHT on restart.

6. Deprecation Path

While this PR maintains the legacy TCP stack for immediate backward compatibility, it marks the start of a transition. The custom TCP call_peer/answer_peer logic is essentially in maintenance mode, with libp2p (and its native QUIC/encryption support) being the path forward.

Acknowledgements

Tasks

  • Libp2p network stack for legacy-compatible peer loops
  • Integrate libp2p network stack into client / main loop
  • autoNAT (automatically detects if we are behind a NAT or firewall)
  • Find relay and become reachable behind NAT
  • Hole punching via DCUtR
  • Automatic peer discovery with Kademlia
  • Enable Ping for tracking peer liveness and keeping ports open
  • Enable UPnP for gracefully requestion ports to open
  • Configure bootstrap nodes
  • Investigate and select other libp2p features
  • Persist own identity / ephemeral / new identity
  • Fuzz HandshakeData codec
  • Clear standing by Multiaddr
  • Dial Multiaddr
  • Documentation
  • Testing testing testing
  • Persist address book
  • CLI command to manually probe NAT status
  • CLI command to reset relay reservations
  • Percolate and enforce bans by Multiaddr, both from CLI and from degraded standing
  • diagnostics+monitoring and integration with neptune-dashboard
  • Respect CLI arguments ban, restrict_peers_to_list, max_connections_per_ip (if possible), handshake_timeout(?), reconnect_cooldown(?)

@aszepieniec

Copy link
Copy Markdown
Contributor Author

Considering mDNS: I've decided against activating it and here is why.

  • It does not work over the internet. It is a LAN-only thing.
  • It floods the LAN and network operators don't always appreciate that.
  • It requires more resources from the node as well as from developers and maintainers.

@aszepieniec

Copy link
Copy Markdown
Contributor Author

Against libp2p_memory_connection_limits: Yes we do want to prevent malicious peers from forcing us to run out of memory, but this crate imposes memory limits on the entire memory consumption of the entire process. Except, we need to support mempools that can reach gigabytes in size without starting to kill peer connections.

@aszepieniec

Copy link
Copy Markdown
Contributor Author

Against libp2p::perf amd libp2p::metrics: poorly documented, unclear benefit.

Against libp2p::gossipsub and libp2p::request-response: these are the primitives you build a composite protocol out of, and we would definitely that if we were to re-implement the synchronization and state replication logic at the libp2p level. However, we already have the peer loop and it looks like we can use that with barely any changes. Besides that, the relevant question would be, what data are we supposed to transmit through gossipsub and request-response. Right now, I can't think of anything. If in the future we need to transmit some data and the peer loop is not the right place, then we can consider gossipsub or request-response instead.

@aszepieniec
aszepieniec force-pushed the asz/libp2p branch 3 times, most recently from ba55d75 to fab1dc4 Compare January 12, 2026 14:56
aszepieniec and others added 22 commits January 28, 2026 17:22
This commit changes the type of command-line argument `peers` from
`Vec<SocketAddr>` to `Vec<Multiaddr>`, where the second item type is inherited
from libp2p.

https://docs.libp2p.io/concepts/fundamentals/addressing/

Users can specify peers in the multiaddress format or in the legacy format.

The existing peer-to-peer related functions continue to work over the subset of
CLI peers that are convertible to `SocketAddr`s.
The `instance_id` is a random integer whose main purpose is to determine if you
are connecting to the same peer twice or connecting to yourself. To achieve this
goal it must be set uniformly at random at startup.

The `PeerId` from libp2p is used for the same purpose and additionally links the
peer to a public key. In the words of the documentation,

> As well as serving as a unique identifier for each peer, a Peer ID is a
> verifiable link between a peer and its public cryptographic key.

source: https://docs.libp2p.io/concepts/fundamentals/peers/#peer-id

The trouble is that the legacy parts of the code, notably the `HandshakeData`,
require the `u128` `instance_id`. Therefore, a deterministic map is implemented
and used that sends the `PeerId` to a `u128`.
Implement a libp2p-based networking stack for use alongside the legacy network
layer. This commit establishes the "Stream Hijacking" pattern, allowing
multiplexed libp2p substreams to be bridged into the existing Neptune peer
event loops.

As a result, the existing peer loop can be plugged in to a libp2p network stack
with zero modifications (and zero code duplication), and run in parallel with
peer loops spawned by the legacy network stack.

New files:

 - `actor.rs`: Implement the libp2p Adapter to coordinate the Swarm and
   dispatch verified connections to the main loop.
 - `bridge.rs`: Add `PeerStream` and `bridge_libp2p_stream` to provide
   a transport-agnostic interface for the PeerLoop.
 - `gateway.rs`: Define the `StreamGateway` (NetworkBehaviour) and
   `GatewayHandler` to manage lifecycle and event routing.
 - `handshake_upgrade.rs`: Implement symmetric CBOR identity exchange
   using a project-agnostic `/id/cbor/1.0` protocol ID.
 - `stack.rs`: Define `NetworkStackEvent` with boxed variants to optimize
   memory layout for diverse protocol signaling (Identify, Relay, Dcutr).

Co-authored-by: Gemini <gemini@google.com>
 - Add constructor for Actor, so that the main loop can build one.
 - Use ArcSwap for lock-free updates of HandshakeData (which depends on
   blockchain tip).
 - Add command-based interface for Listen.
Implement `get_own_handshakedata_sync` to allow the network actor to
retrieve handshake templates without blocking the async executor.

 - Add `try_lock_guard` to `AtomicRw` to support non-blocking state access.
 - Implement a stale-while-revalidate cache using `std::sync::RwLock`.
 - Ensure handshake timestamps are finalized immediately before use to
   maintain freshness despite potential lock contention.

The purpose of all these changes is to anticipate integrating the application
stack with the network stack. The fundamental problem is this: the libp2p
traits are synchronous, we want them to serve handshake data, and there are
(until now) no synchronous means to get it. In short, these changes allow us to
bypass async requirements in libp2p protocol handlers.
Spawn and run the libp2p NetworkActor, which manages the swarm and spawns peer
loops whenever a connection comes in through the swarm (of whenever it is
instructed to dial a remote peer and that connection goes through). This
connection manager lives alongside the legacy connection manager. Both use the
same peer loop.

The peer loop and associated functions are modified not in terms of control flow
but in terms of data types for containers. Specifically, this commit substitutes
the (now legacy) `SocketAddr` for the libp2p `PeerId` for use as the key type in
various maps, starting with the peer map.

The `PeerId` is (when the connection is mediated by libp2p) cryptographically
bound to a public key. If the peer does not possess the matching secret key,
they will not be able to establish the connection. As a result, this key can
better, persistent, tracker of peers' identities. One peer can have multiple
addresses; if they are honest they will have the same `PeerId` and consequently
the node can save work and produce properly decentralized peer set.

Meanwhile, spam and DoS countermeasures continue to operate at IP address level.
So no new attacks are exposed because the attacker needs to everything he needed
to do before plus generate keypairs cheaply.

This commit is breaking because of some minor changes in `pub` structs such as
`PeerInfo`.
 - Fix docstring links.
 - Underscore unused variables.
 - Unmerge imports.
 - Avoid running non-code markdown text area.
Store the node's peer-to-peer identity (public/secret key pair) in a file in the
data directory, so that the next time the node boots it has the same identity.
Add CLI flags that allow the user to:
 - ignore the file and use an ephemeral identity instead;
 - backup the file and use a fresh identity file;
 - use a specified identity file.
 - Add libp2p-autonat for automatic network reachability detection.
 - Implement `handle_autonat_event` to track Public/Private NAT status.
 - Refactor `StreamGateway` logic into a dedicated async handler.
 - Synchronize peer address mapping with connection lifecycle events.

This change allows the node to automatically determine its reachability
status, laying the groundwork for automatic relay reservations when
behind restrictive NATs. The refactored handlers improve the scannability
and maintainability of the NetworkActor's main event loop.
Previously, functions `call_peer` and `answer_peer` would send a `NewPeer`
message to the main loop if sync mode was on; and
`close_peer_connected_callback` would send (still sends) a `DroppedPeer`
message, also if sync mode is on. This distribution of tasks mixes
responsibilities and leads to inconsistent behavior in the context of the new
network stack. In particular, `call_peer` and `answer_peer` are legacy-only
functions whereas `close_peer_connected_callback` is common between legacy and
libp2p network stacks.

Thsi commit moves the responsibility of sending the `NewPeer` message to
`run_wrapper`, which is like `close_peer_connected_callback` shared between
legacy and libp2p stacks. As a result, notifying the main loop of the identity
of the peer loop's counterparty is not the responsibility of the NetworkActor.
Consequently, some functions and structs can be renamed and simplified.

In particular:
 - `PeerTaskMessage::NewPeer`: needs no address; just `PeerId` suffices.
 - `NetworkEvent::PeerConnected`: renamed to `NetworkEvent::NewPeerLoop` and
   associated fields `peer_id`, `handshake`, and `address` are dropped.
 - `NetworkEvent::PeerDisconnected`: no need for this variant.
 - Add `AddressBook` struct to manage and persist peer metadata
 - Implement `prune_to_length` using a multi-factor `PeerScore` (rating protocol
  version, recency, and longevity)
 - Add disk persistence (JSON) with manual `GetSize` implementation for memory
   safety
 - Implement 10KB heap-size limit per peer entry to prevent memory exhaustion
   DoS
 - Read address book at startup; write upon graceful shutdown
 - Update entries with data supplied by peers engaging in the Identify
   subprotocol
Read the address book and make a selection of peers to dial. If underpopulated,
add hardcoded bootstrap nodes. Then dial before the NetworkActor's event loop
starts.
The new field counts the number of failed connection attempts since the last
success. It can be used for a better measure of peer quality, which is what
we end up using when we sample a peer to dial.

Also:
 - Change visibility of test methods.
 - Apply clippy changes.
AutoNAT was already enabled but not properly used. AutoNAT detects whether the
node is behind a NAT. This commit does something with this information, namely
if the node is publicly accessible, it includes that public address in its
Identify handshake.
With this configuration in place, all nodes act as relay servers. Consequently,
Any peer can provide you with a sub-address (under theirs) and relay messages
between anyone who connects to that sub-address and you. The lifetime and
bandwidth of this relay channel is limited, so it is not useful for much besides
direct-channel upgrades (AKA "hole punching").
Implements the logic to establish and maintain reachability when the node is
behind a NAT. Reachability is achieved by asking relay servers for a proxy
address (along with data relaying).

All nodes required to offer this relaying service. However, it is a very
lightweight requirement: there are limits on the number of proxy addresses,
the number of active circuits, the amount of data transferred, etc. These
limits are large enough to enable DCUtR (hole punching coordination) and not
much more.

Specifically, this commit:
 - Monitors AutoNAT status changes to trigger relay requests (if new NAT status
   is "Private") or state clean-up (if new NAT status is "Public").
 - Implements a self-healing relay pool that selects candidates from active
   connections. (All nodes must serve as relays.)
 - Manages relay reservations by tracking `PeerId`, `ListenerId` and "80%-mark"
   -- the last field is used to seamlessly set up a new relay before the old one
   expires.
 - Handles unexpected listener closures by automatically seeking  replacement
   relays to maintain a target pool size.
Adds the event handler for Direct Connection Upgrade through Relay (DCUtR) to
the NetworkActor. Tracks success and failure of TCP hole-punching attempts emits
log messages.

Note that DCUtR was already enabled as an automatic side-effect of a) including
its Behaviour in the NetworkStack, and b) preparing the scaffolding: autoNAT,
relay client, relay server. This commit really only adds the log messages.
Enable Kademlia DHT for peer discovery with ConnectionLimits behavior to
enforce network diversity and resource safety.

 - Add libp2p-connection-limits to the network stack.
 - Configure max_established (`--max_num_peers`) and max_per_peer (1) limits.
 - Bridge Identify and Kademlia to automate 'one-hop' peer discovery.
 - Add error handling for 'ListenError::Denied' to monitor the
   ConnectionLimits bouncer.

This change ensures the node maintains a diverse set of unique neighbors,
maximizing routing table health while preventing peer connection hogging.
Apparently, the expression `..Default::default()` calls the `Default`
implementation for `Self`, instead of the `Default` implementations of the
fields it is used for. As a result, the use of `..Default::default()` in
`Self::new` would call `Self::default()` which would call `Self::new` etc.
until stack overflow.
aszepieniec and others added 19 commits January 28, 2026 17:31
Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Also: activate derivation of `strum::EnumCount` for `RelayStrategy` on feature
`mock-rpc`.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Instead of with new listen address. Confirmed external addresses are more
trustworthy than new listen addresses, but these events take longer to trigger.
We want to react fast (so swarm actions happen in `NewListenAddress`) but
accurate information to the user (so display data is only updated in
`ConfirmedExternalAddress`).

Also, filter out multi-hop circuit addresses. Not necessary under normal
circumstances, but if the address space is polluted with multi-hop circuit
addresses (as they are at the time of writing) then those addresses can persist.
On debug level, log why an incoming connection is rejected (AKA
bounced).
Change the `bounce` implementation to allow for incoming connections via
a single hop. Otherwise, we cannot receive or open up connections to
nodes where a relay is required.
New confidence: 2. Previous: default (3). So if two nodes say you're public,
you're public.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Initiator of handshake upgrade protocol should not be the connection initiator
because that does not work for hole punching (both peers are initiators).
Instead, the peer with the lower `PeerId` is the designated initiator, and the
other (if the `PeerId` is different) is the responder.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Add CLI support to specify own public IP addresses. If set, this list will
instruct the NetworkActor to disallow adding automatically-discovered external
addresses to the swarm.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Fix a bug whereby a circuit address would be built on top of a "naked"
Multiaddr, which is "just" a PeerId: e.g., /p2p/<PeerId>/. These are addresses
as far as libp2p is concerned and so they can be shared and announced via
Identify and Kademlia. However, for circuiting the transport address must be
specified because otherwise you generate a `MissingRelayAddr` error.
Change data type of `NetworkActor` field `active_connections`: instead of
holding one `Multiaddr` for each peer, store a
`HashMap<ConnectionId, Multiaddr>` per peer, and instert and remove from this
map when connections are established and torn down, respectively.

As a result, we can select from a variety of addresses when requesting relay
reservation, and avoid ones that are guaranteed not to work. Previously, that
filter would rule out the entire peer.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Avoid sending local addresses in Identify messages.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Fixes (hopefully) dcutr / hole-punching.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
While there is an argument to be made in favor of being a responsible peer and
not announcing addresses that are bound to fail, it is imperative for
hole-punching that some direct IPs (image of NAT router) are announced. So when
the NAT status changes to Private, do not remove external addresses -- there
might be useful ones in there.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>
Ensure all relevant git commits (for isntance, those containing motivations)
live on in some form or another in the documentation. Anticipate a squash.
@aszepieniec

Copy link
Copy Markdown
Contributor Author

feat: libp2p Network Stack

Squash and merge of PR #828: #828.

This commit introduces the libp2p network stack. Libp2p is a library for peer-to-peer
protocols such as blockchains. It comes with a variety of off-the-shelf subprotocols
for things like NAT traversal and peer discovery. This PR activates a libp2p network
stack while reusing the same old battle-tested blockchain peer loop. For the time
being, the legacy network stack's control flow pathways into the blockchain peer
loop (specifically, those contained in connect_to_peers.rs) remain active, but going
forward these are considered deprecated and scheduled for removal.

The new module is documented verbosely in the documentation under
neptune_core/network_stack.md.

@aszepieniec
aszepieniec merged commit cfed34a into master Jan 28, 2026
5 of 11 checks passed
@Sword-Smith
Sword-Smith deleted the asz/libp2p branch February 4, 2026 09:49
Sword-Smith added a commit that referenced this pull request Feb 4, 2026
* fix(wallet-server): fix error handling in block_selector

* feat(json_rpc): Add methods for off-node wallets (#777)

* feat(json_rpc): Add `RestoreMembershipProof` call

* feat(json_rpc): `GetBlocks` call on Wallet and prepare types for `submitTransaction`

* style(json_rpc): Simplify error conversion from RpcError to JsonError

* feat(json_rpc): Add `SubmitTransaction` call and `SubmitTx` to channel

* feat(json_rpc): Test an off-node wallet scenario and refactor some RPC types

* feat(json_rpc): Expose appendix on BlockKernel

* test(json_rpc): Cleanup and use deterministic timestamps on off_node test

* feat(json_rpc): Avoid TransactionKernelProxy impl and ProofCollection-capability on tests

* fix: Implement `Debug` for `ArchivalBlockMmr`

* refactor: Harden and scrutinize bisection search for confirming block

* docs: Explain that transparent txs are more expensive

Because of the added announcements, transparent transactions require a
higher transaction fee than non-transparent, regular transactions.

* feat(json_rpc): Add `getBlockTemplate` and `submitBlock` to `mining` namespace

* feat: Crate-level `from_biguint` on `Difficulty` and use `RpcDifficulty`

* feat: Crate-level `from_biguint` on `ProofOfWork`and use `RpcProofOfWork`

* test(json_rpc): Add `solve` utility from metadata

* feat(json_rpc): Test miner scenarios and return proper errors with `submit_block`

* style(json_rpc): Improve error message of `InsufficientWork`

* style(json_rpc): Improve error message and remove redundant cfg test

* feat(json_rpc): Return BlockHeight on `Height`

* feat: Proper handling of vec_u32 on ActiveWindow

* test: Remove a redundant test and test roundtrip of Diff and Pow

* docs: Document required size of static allocation

Also, slightly clean up inline stack annotations by removing irrelevant
entries, and inline some used-once TASM.

changelog: ignore

* sec: Limit announcement size

Potential DoS vector. Change applied to `peer_loop`, not `is_valid`.

Credit to AllFather team.

* sec: Limit bincode frame size

Prevents short malicious messages from allocating a lot of memory.

Credit to AllFather team.

* sec: Hold permit over `answer_peer`; release after

Countermeasure against semaphore starvation attack.

Credit to AllFather team.

* sec: Track and limit memory consumption in fork reconciliation procedure

Potential DoS vector.

Credit to AllFather team.

* style: Run `cargo fmt` and `clippy`

* fix: Add missing function arguments and match cases

* refactor: Move and clarify const for mempool/tx-relay policy

Const `MAX_ANNOUNCEMENT_MESSAGE_SIZE` is does not contribute to block
(in)validity, but is enfored at the peer-to-peer level as a transaction relay
and mempool insertion policy. This commit moves the const definition to the
file where it is used, `peer_loop.rs` and clarifies its purpose in the
docstring.

* refactor: Define fork reconciliation policy const in terms of consensus const

Const `MAX_BLOCK_SIZE_IN_FORK_RECONCILIATION` is enforced in the course of fork
reconciliation. Previously it was set to 10 MB, now it is set to 2x 8 MB, where
the 8 MB comes from the block size.

Also: clarify purpose.

* refactor: Drop fork reconciliation memory limit

There is already a memory limit being enforced implicitly, namely through:

1. Requiring that every block have a size less than or equal to
   `MAX_BLOCK_SIZE_IN_FORK_RECONCILIATION`.

2. Requiring that the number of blocks being reconciled does not exceed
   `self.global_state_lock.cli().sync_mode_threshold`.

This commit removes the memory limit in `try_ensure_path` as well as the state
variable used to track memory use for fork reconciliation.

* refactor(sync): Add data structures for sync loop

* fix(sync): Fix logic errors in `BitMask`

* fix(`BitMask`): Fix out-of-bounds error

* fix(`BitMask`): Unset excessive bits in new last limb

* fix(`BitMask`): Avoid skipping offset increase

* refactor(`RapidBlockDownload`): Restructure state and function signatures

* test: Implement pseudorandom sampler for `Block`

Pseudorandom blocks are needed for a test (unrelated to fuzzing). Implementing
the pseudorandom sampler for `Block` percolates to similar implementations for
constituent components.

* refactor: Add functionalities for rapid block download

* test(rapid-block-download): Can retrieve blocks iff stored

* fix(rapid-block-download): Record and store received block correctly

* test(rapid-block-download): Verify that receiving all blocks completes the task

* test(rapid-block-download): Verify that receiving the same block twice is ok

* test(rapid-block-download): Add test about updating tip while syncing

* refactor(rapid-block-download): Add stub implementation of sync loop

* test(rapid-block-download): Test sync loop for good peers

* refactor(rapid-block-download): Accommodate flaky peers

When peers are flaky, they might not respond to block requests. This commit
hardens the asynchronous architecture of the sync loop to account for peers that
can be flaky. Specifically:

 1. Prefer `try_send` over `send` when sending messages from the sync loop to
    the main loop.

The latter blocks if and as long as the channel capacity is maxed out. If the
counterpart also blocks (for the exact same reason or any othe reason), then
you have a deadlock. In contrast, `try_send` returns immediately and yields an
error if the transmission was not successful.

 2. Factor out the function that sends tip-successors to the main loop in order
    into an asynchronous task.

This function might take a long while to finish, particularly if it spends time
waiting on `send` to complete. And in this particular case we definitely want to
use `send` rather than `try_send` because we want to ensure that these blocks
arrive at the main loop in good order.

* feat(rapid-block-download): Resume sync from stored state

When syncing, downloaded blocks are stored to a temp directory. If the node
crashes or even is powered down gracefully, those files might still be there.
So upon (re-)booting the sync loop, check if there are any blocks already
in the temp directory so that we can avoid downloading them again.

* refactor(rapid-block-download): Drop functionality for sanctioning peers

There is a potentially very large temporal distance between the peer's behavior
(sending the block) and judgment (block was valid or invalid). In order to
punish or reward peers appropriately, we would have to carry around this
information (who sent the block) until we can act on it (by punishing or
rewarding them). More information carried across time increases the complexity
of an already complex subsystem.

* feat(rapid-block-download): Sync from dynamic peer set

Enable two message types to the sync loop, for adding and removing peers. Then
the sync loop has to sync from whatever peers are available.

To avoid a situation where one loop sends more messages than it receives for a
sustained period of time, the code is adjusted in two ways.

 1. The block requests are not sent out individually but collectively in a
    batch.

 2. Transmissions of block requests and block responses are sent without
    blocking and are allowed to fail if the channel is out of capacity. All
    other message types are sent in a blocking way ensuring that they are
    received in good order.

* fix(rapid-block-download): Wait for subtask before terminating sync loop

There is pathway to escape the sync loop when for a certain period no blocks
have come in. Previously, that would trigger the termination of the sync loop.
Now, that's only true if the tip-successors subtask is not active. So if it is,
we wait for it to finish first.

Also:
 - Figure out early in the pipeline that we do not need to send out block
   requests because all blocks are in already.
 - Be verbose about failures to send tip-successors to the main loop before
   retrying.

* CI: Run parallelism-unfriendly tests in sequence

* feat(rapid-block-download): Track moving target when syncing

Specifically, if new blocks are announced while syncing, update the sync target
to the new blockchain tip.

* feat(rapid-block-download): Sync from peers who themselves are syncing

The general strategy is for peers who cannot respond to a block request to reply
instead with a snapshot of their current sync state. Then the original peer can
sample a block that a) they don't have yet, and b) the also-syncing peer is
capable of serving.

* style(rapid-block-download): Sanitize log output

Make less verbose.

* refactor(rapid-block-download): Rename `BitMask` -> `SynchronizationBitMask`

Avoid overly generic names for special-purpose structs.

* refactor(rapid-block-download): Represent synchronization bit mask sparsely

Rather than explicitly representing a whole bunch of leading ones, record the
lower bound where they stop. This way, only the bits between the lower and upper
bounds are represented explicitly. This change in representation percolates to a
lot of algorithms, which are changed accordingly.

* perf(rapid-block-download): Prefer `VecDeque` for `pop_front`

When syncing progresses in good order and the first 32 or so blocks are
processed, we need to get rid of the first limb in the synchronization bit mask.
Previously, this mask stored a `Vec` of `u32`s and would `remove(0)` when
necessary. Now it is a `VecDeque` instead, which comes with the much cheaper
`pop_front()`.

* feat(rapid-block-download): Report status

Motivation: to show in the dashboard (or other UI) how far we are in syncing.

* refactor(rapid-block-download): Validate blocks prior to sync->main transmission

When the sync loop receives blocks, it has no way of verifying their validity.
The main loop expects incoming blocks to be valid (ignoring blocks forwarded
from a peer to the sync loop). Therefore, the sync loop should verify the blocks
prior to sending them in order to the main loop.

To facilitate this check, this commit introduces a new object, `BlockValidator`,
which contains the function that tests blocks for validity. In production, this
object always wrap the canonical block validation function. But in test mode,
the test setup may inject a bogus validation function. Moreover, it means that
the sync loop can make abstraction over block validation logic.

Since blocks are validated relative to their predecessors, the sync loop now
stores the current tip (previously it was only the current tip height). The
alternative would be to fetch it from the main loop or pass a mutex. Whenever
the sync loop is running, the main loop is only allowed to set the tip on the
orders of the sync loop. So in particular the sync loop always knows what the
tip is by just remembering its most recent order. There is no need for state
reconciliation this way.

* refactor(peer_loop): Modify height-tracker only if block handling success

Struct `MutablePeerState`, of which one object lives in each per loop by the
name `peer_state_info`, has a field `highest_shared_block_height`. It is used
for two things.

 1. In the shallow fork reconciliation mechanism, AKA. `try_ensure_path`, after
    successfully passing a path of blocks to the main loop, control must make a
    decision whether to send a new `BlockHeightRequest` to the peer in question.
    It makes this decision based on whether the biggest block height in the path
    of blocks passed to the main loop is smaller than the peer's
    `highest_shared_block_height`, which could have been set in the mean time by
    a `BlockNotification`.
 2. When the peer loop receives a `Block` from the main loop, control needs to
    decide whether to send a `BlockNotification` to said peer. It makes this
    decision based on whether the new block's height is larger than the peer's
    `highest_shared_block_height`.

Until now, it was set in two circumstances.

 1. If the peer is notified of the `Block` that just came in from the main loop.
 2. The peer sends a `BlockNotification`.
 3. When a `Block` from a peer is received, and only if no shallow fork
    reconciliation is running.

This commit changes (3) into

 3'. When a `Block` from a peer is received, leading to the completion of a
     shallow fork reconciliation process (which could be trivial if we are only
     one block behind) which itself results in one `Block` or batch of `Block`s
     being sent to the main loop for processing. In this case, the
     `highest_shared_block_height` is set to the largest height of all blocks
     thusly sent to the main loop.

This change anticipates interfacing with the sync loop, which will request
blocks out of any order. When those blocks come in, the
`highest_shared_block_height` should *not* be set. Moreover, when those blocks
come in, the shallow fork reconciliation mechanism should bot be activated
either. So it makes sense to tie these two non-events together.

The return type of `try_ensure_path` was modified to communicate the potential
new highest shared block height to the caller, which is the peer event loop.
This way, all modifications to `highest_shared_block_height` happen in this
loop (besides initialization, where the value is obtained from the handhsake).

* refactor(sync-challenge): Increase timeout from 45s to 90s

Two out of three bootstrapper nodes listed in the README.md consistently respond
to the sync challenge after the timeout period has expired. The third one (which
consequently carries all the load for syncing nodes) responds on time *some of
the time*. So for whatever, the sync challenge response is hard to produce
and/or deliver.

This commit doubles the timeout period.

* refactor(rapid-block-download): Relay sync blocks to sync loop

Modify the peer loop: upon receiving a block, check if we are in sync
mode. If so, then (depending on the block's features) relay it to the
main loop without validation, so that it relays it to the sync loop.

Modify the channel from peer loop to main loop: add two message types,
one for a new sync target (which extends the chain) and one for a new
block (living somewhere in the middle of the chain). Also: put the
data structures specific to main loop <-> peer loop interaction into a
separate file.

Modify main loop: add optional handle on sync loop to state, and handle
the new messages by relaying them if the handle is present.

* refactor(rapid-block-download): Encapsulate handle on sync loop with channels

Deter channel mis-use. Remove concurrency patterns from interface.

* feat(rapid-block-download): Integrate sync loop into main loop

Switch to the new (parallel) method for syncing.

Also: add new main-to-peer and peer-to-main messages for support.

* ux!: Report sync status with more precision

Specifically, modify dashboard overview data field `syncing` into `sync_status`
which is a new enum where some variants have associated data. Render this field
correctly in the dashboard. The RPC server obtains the value of this field from
network state. It is updated by the main loop or peer loops. To get the progress
indicator, a message must be sent to the sync loop. The RPC handler sends a new
message to the main loop, asking it to update status indicators. The main loop's
handler for this message sends a message to the sync loop (if any) to compute
sync progress.

* refactor(rapid-block-download): Replace existing sync mechanism with sync loop

Integrate sync loop deeper into client. Delete functions and structs related to
old sync mechanism.

* refactor: Delete newly unused main-to-peer message `RequestBlockBatch`

The message had been used in the previous sync procedure, was deprecated by the
new sync process, and is now deleted.

* refactor(sync-loop): Punish unresponsive peers

Whenever the sync loop is active, every 10 seconds that a peer does not respond,
it will be punished.

This mechanic is achieved by tracking two new pieces of information, stored on
the `PeerSyncState`. Both are optional timestamps: `last_response` and
`last_punishment`. The former tracks the last any message from the peer (so that
punishments do not happen before they need to). The second tracks the last time
the peer was punished (so that punishments do not happen too frequently).

* feat(sync_loop): Sync from syncing peers

When syncing, the node now asks peers who are also syncing about blocks they can
serve. Upon being asked for a block by height, the client now first checks the
archival state. If the archival state does not have the block, and if the sync
loop is active, then the block request message is passed on to the sync loop.
The sync loop responds with the block if it is present, or with the
synchronization bit mask (AKA coverage) if not.

Caution: this commits adds a new `PeerMessage`: `SyncCoverage`, whose associated
data is the synchronization bit mask indicating which blocks are present and
which are absent.

* refactor!: Drop deprecated struct `ClaimedSynchronizationState`

Struct was used by previous mechanism to settle race conditions. No longer
necessary.

Breaking because the struct was marked `pub`.

* fix(`NegativePeerSanction`): Account for new variant

* feat(CLI): Add command `no-resume-sync` to avoid resuming a previous sync

Downloaded but unprocessed blocks are stored in a temporary directory. By
default, if a new sync is spawned it resumes the sync based on the blocks
stored in that directory. The new command allows the user to specify that
no resumption is allowed. If a new sync process is started, it must start
from scratch.

* refactor(CLI): Ignore bad standing when peer is CLI argument

* fix(sync-loop): Only send punishment message when not empty

* docs(peer_loop): Motivate punishment policy

* fix(sync_loop): Record timestamp of last punishment

Avoid punishing again too soon.

* fix(sync_loop): Avoid spamming the main loop with punishment messages

If a message send fails, wait a while before trying again.

* docs(sync_loop):

* refactor(sync_loop): Improve tenacity of sends on heavily loaded channels

* log(sync_loop): Increase verbosity of debug logging

* refactor(sync_loop): Restart tip-successors subtask only if there is work to do

* refactor(sync_loop): Start block requester task only if there is work to do

* feat: Tolerate reorgs and warn user about undeletable corrupt data

Tolerance for reorgs is already implicitly present: a sync failure will lead to
a new `RequestBlockNotification` which should trigger a whole new sync. In order
for the new sync to avoid using potentially corrupt data, it is important to
avoid resuming the failed sync. By default, the sync loop deletes the temporary
directory holding its state, but this deletion may fail. In this case, inform
the user about deleting it manually.

This commit adds the informational log message (as an error message).

Also: document behavior.

* feat(sync_loop): Sync across forks

When syncing, you might have to walk back to find the latest universal common
ancestor (LUCA) block before walking forward. This commit effects this behavior.

Specifically:
 - The lower bound of the synchronization bit map (AKA. "coverage") is initially
   zero, instead of current-tip-height.
 - Whenever a block comes in and sync mode is active, the node tests two
   properties: a) do we know about this block already?, and b) is it on the
   canonical chain? If the answer to both is yes, then the lower bound is
   fast-forwarded to the given block height. If not, the block is treated as a
   regular sync block.
 - Immediately after the sync loop is spawned, the node asks the peer that
   triggered the spawn for its block at the current tip height.

These changes result in the following emergent behavior.
 - If the peer responds with the same block that we have as our tip, then the
   sync loop is fast-forwarded to our own current tip and the entire subsequent
   process proceeds as before.
 - If the peer responds with a different block from the one that we have as our
   tip, then that indicates that the peer is on a different fork from us and we
   have to walk back to find LUCA. In this case we do not fast-forward the sync
   immediately and let the sync loop query uniformly random blocks between the
   lower bound (now still zero) and the upper bound (target block height).
   Whenever a block comes in that we already know about and is canonical, then
   we fast-forward to that height. The resulting protocol is running a
   probabilistic bisection search for LUCA. When LUCA is found, the sync loop is
   fast-forwarded to that height; and when the LUCA-successor of the peer is
   received, the sync loop will trigger a tip-successors subtask which calls the
   state update function `set_new_tip` on this block, and possibly many
   successors as well.

Also: document behavior.

* refactor(sync): Restrict conditions for spawning sync loop

Require both:
 - no sync loop already running
 - no sync anchor present

In principle, it does not matter as these two things should always
be set or unset in tandem. However, it cannot hurt to be explicit
about the requirement.

* refactor(sync_loop): Avoid letting status update requests clog channel

Send them only if the channel is empty.

* refactor(`SyncProgress`): Change meaning of `SyncProgress`

Previously, `SyncProgress` was a number indicating progress relative to the
interval of missing blocks. However, this turns out to be a very noisy and
unreliable number in the context of sync across forks when LUCA is unknown.
LUCA is generally unknown.

Phrased differently, if the magnitude of the missing interval were known, then
LUCA would be known as well; and finding LUCA is precisely the sync loops job.

So instead, the `SyncProgress` captures the total number of blocks downloaded
relative the total number of blocks on the chain.

* refactor(sync_loop): Clarify and restrict conditions for termination

Specifically, no tip successors task can be running -- even if we have not seen
a block in ages, we must finish the tip successors task before we can terminate.
Moreover, download cannot be finished. If it is, then a tip successors task will
be spawned to process them.

Also: wait progressively longer between attempts to send return code to main
loop.

* refactor(sync_loop): Add backstop to restart tip-successors subtask

Add a probably unnecessary backstop to rescue the sync loop from a state it
should never be in. Specifically, if all blocks were downloaded, but not all
were processed, and no tip-successors subtask is running. Then make it run.

* fix(sync_loop): Guard against slow main loop

The main loop can get slow, particularly when it is applying the state update to
many many UTXOs. For such tasks it holds the write lock, meaning that the main
loop cannot process new messages -- not even messages coming from the sync loop.

This commit changes the sync loop in two ways.

 1. Make the message `RequestBlocks` more insistent. If sending fails, do not
    just give up but try again after a while Do this up to 100 times. If sending
    still does not work, do not record the last request timestamp as now.
 2. As a precondition to early termination of the sync loop in the event of no
    observed activity from peers, also check if such activity was provoked
    recently. If the last request had been sent too long ago, then it makes
    that there is no response from the peers. By not terminating under these
    conditions, the sync loop is giving the main loop a chance to finish.

Also, make the early termination due to lack of peer blocks possible only after
some time after sending the requests.

* fix(sync_loop): Try harder to ensure sending tip-successor

* refactor(sync_loop): Drop early exit backstop

Previously, when there was no sign of life from peers for too long, the sync
loop would terminate. This escape hatch has been removed.

First, it was designed to recover from an inconsistent state, which should not
be reachable to begin with.

Second, the criteria for early exiting are met in practice falsely when there
is heavy load on the communication channels. (For instance, this can happen
when the main loop is busy processing blocks and each state update takes a
while because there are many UTXOs to keep track of.)

* refactor(sync_loop): Insist sending in separate task

When sending insistently (in case of failure, wait a while before trying again
some number of times), prefer a separate task so that control can return to the
loop.

* refactor(sync_loop): Drop unnecessary async decorator on async-wrapper

Async wrapper which spawns async task does not itself need to be async. In fact,
if it is, clippy complains.

* refactor(sync_loop): Move `BlockValidator` and reduce scope

Do not expose `BlockValidator` outside of module `sync_loop`. Let the interface
to the sync loop, which is `SyncLoopHandle`, take a `Network` object instead.
The correct `BlockValidator` is then inferred from the passed `Network`.

This change reduces interface complexity.

* Update lock_script.rs

reversing arbitrary is pointless

* feat: DbtMap without `remove` functionality

Intended for a new table in the wallet database to allow for a fast
lookup from AOCL leaf index to MonitoredUtxo. The `MonitoredUtxo`s are
stored in a `DbtVec` list which is indexed by `u64`, and the AOCL is
also indexed by `u64`. This means that the motivation for and use case
I currently have in mind is `DbtMap<u64, Vec<u64>>`, where the inner vec
will might multiple entries in the case of reorganizations.

The reason that the `DbtMap` does not have remove functionality is that
we want the option of iterating through all keys. So all keys are stored
in an underlying `DbtVec` which only supports `pop`, `push`, and `set`
operations. Since an arbitrary element cannot be efficiently removed
from `DbtVec` this means that the same slow running time would apply for
`remove` on `DbtMap`.

One work-around would be to use `DbtVec<Option<u64>>` for the underlying
list of keys and mark deleted keys as `None`. But this would result in
incomplete data deletion when `remove` was used (a `None` element would
remain in underlying `DbtVec`), and it would result in linear instead of
constant running time for the map's `len` and `is_empty`
implementations. Instead of leaving these footguns lying around, I
though it'd be better to restrict functionality. The intended use case
doesn't need `remove` anyway.

Cf. #786.

* refactor(wallet)!: Fast lookup from AOCL leaf index to `MonitoredUtxo`

Add a new table to the wallet database: `aocl_to_mutxo` that allows for
a fast lookup from an AOCL leaf index to a monitored UTXO. The mapping
maps a AOCL leaf index to a monitored UTXO list index such that a
monitored UTXO can quickly be found given an AOCL leaf index.

Also refactors `MonitoredUtxo` since prior to this commit, the AOCL leaf
indices of the monitored UTXO only lived in the mutator set membership
proofs. Since monitored UTXOs only represents *mined* transactions,
sender randomness, receiver digest, and AOCL leaf index will always be
known when the monitored UTXO is inserted into the database. Therefore,
we can add these values directly as fields in the monitored UTXOs.

This fast lookup allows certain operations in the wallet state update to
be done faster, specifically the check for potential duplicates can now
be done without looping over all monitored UTXOs.

Missing: Wallet database migration logic to convert the `MonitoredUtxo`
values that are stored in nodes' wallet databases. This is not added in
this commit since I want to add another lookup for monitored UTXOs: One
from absolute index sets to MUTXO list indices.

Cf. #786 and #785.

* refactor!(wallet): Fast index set -> monitored UTXO lookup

Add a new table to the wallet database to allow for fast lookups from
absolute index sets to monitored UTXOs.

This is used to speed up the `WalletState::scan_for_spent_utxos` method
that is called each time the state is updated with a new block.

This method goes from haven an O(N) runtime to having a O(L) runtime,
where N is number of monitored UTXOs, and L is number of inputs in the
applied block.

* bench(wallet): Add bigger case for wallet state update

Added because we are optimizing wallet update speed cf. #786 and #785.

Reveals that new lookup tables in the database gives a 44 % speedup on
the application of a new block. However, the method
`WalletState::update_wallet_state_with_new_block` actually gets a 72 %
speedup as you can deduce if you do the math on the numbers below (1000
entries, 10 new is 16ms for the state update and 16ms for the MSMP
maintanence).

I had hoped for a bigger speedup. Perhaps the benchmark is too small? Or
perhaps we simply have to get rid of the constant membership
maintanence? As the benchmark results below reveal, half of the time
applying a state update to the wallet is now spent fetching the membership
proofs from the archival mutator state. Since the node carries around an
archival mutator set, no membership maintanence is required! So there's
another 100 % speedup there for when we stop maintaining membership
proofs after each block.

Benchmark run on megingjord:
`master`
wallet_state                                                      fastest       │ slowest       │ median        │ mean          │ samples │ iters
├─ maintain_membership_proofs                                                   │               │               │               │         │
│  ╰─ maintain_msmps                                                            │               │               │               │         │
│     ├─ apply_block2_maintain_msmps_400_10                       363.3 ms      │ 394 ms        │ 385.7 ms      │ 384.1 ms      │ 10      │ 10
│     ├─ apply_block2_no_maintain_msmps_400_10                    10.36 ms      │ 24.91 ms      │ 11.24 ms      │ 12.85 ms      │ 10      │ 10
│     ╰─ apply_block2_no_maintain_msmps_1000_10_with_no_update  N/A -- crashes│
│     ╰─ apply_block2_no_maintain_msmps_1000_10_with_msmp_update  41.81 ms      │ 89.19 ms      │ 43.48 ms      │ 47.83 ms      │ 10      │ 10
╰─ resync_membership_proofs                                                     │               │               │               │         │
   ╰─ sync_200_msmps_over_10_blocks                                             │               │               │               │         │
      ╰─ resync_msmps_bench                                       294.3 µs      │ 420.9 µs      │ 309.4 µs      │ 318.3 µs      │ 10      │ 10

`perf-wallet`
wallet_state                                                      fastest       │ slowest       │ median        │ mean          │ samples │ iters
├─ maintain_membership_proofs                                                   │               │               │               │         │
│  ╰─ maintain_msmps                                                            │               │               │               │         │
│     ├─ apply_block2_maintain_msmps_400_10                       318.5 ms      │ 361.3 ms      │ 321 ms        │ 327.1 ms      │ 10      │ 10
│     ├─ apply_block2_no_maintain_msmps_400_10                    7.588 ms      │ 25.01 ms      │ 8.259 ms      │ 9.854 ms      │ 10      │ 10
│     ├─ apply_block2_no_maintain_msmps_1000_10_no_msmp_update    16.73 ms      │ 21.8 ms       │ 17.89 ms      │ 18.54 ms      │ 10      │ 10
│     ╰─ apply_block2_no_maintain_msmps_1000_10_with_msmp_update  27.15 ms      │ 75.78 ms      │ 30.83 ms      │ 34.86 ms      │ 10      │ 10
╰─ resync_membership_proofs                                                     │               │               │               │         │
   ╰─ sync_200_msmps_over_10_blocks                                             │               │               │               │         │
      ╰─ resync_msmps_bench                                       281.7 µs      │ 588.5 µs      │ 291.8 µs      │ 322.8 µs      │ 10      │ 10

`perf-wallet` factor out MSA calculation:
wallet_state                                                      fastest       │ slowest       │ median        │ mean          │ samples │ iters
├─ maintain_membership_proofs                                                   │               │               │               │         │
│  ╰─ maintain_msmps                                                            │               │               │               │         │
│     ├─ apply_block2_maintain_msmps_400_10                       337.8 ms      │ 344.5 ms      │ 338.8 ms      │ 339.3 ms      │ 10      │ 10
│     ├─ apply_block2_no_maintain_msmps_400_10                    7.485 ms      │ 24.8 ms       │ 7.866 ms      │ 9.565 ms      │ 10      │ 10
│     ├─ apply_block2_no_maintain_msmps_1000_10_no_msmp_update    16.24 ms      │ 20.69 ms      │ 16.76 ms      │ 17.44 ms      │ 10      │ 10
│     ╰─ apply_block2_no_maintain_msmps_1000_10_with_msmp_update  25.22 ms      │ 69.39 ms      │ 29.72 ms      │ 33.14 ms      │ 10      │ 10
╰─ resync_membership_proofs                                                     │               │               │               │         │
   ╰─ sync_200_msmps_over_10_blocks                                             │               │               │               │         │
      ╰─ resync_msmps_bench                                       282 µs        │ 425.4 µs      │ 294.7 µs      │ 306.2 µs      │ 10      │ 10

* perf(archival_state): Avoid recalculating mutator set after

* test(wallet): Real v1->v2 migration data

From testnet, on a node of mine.

* chore: Make linter happy

* refactor(wallet): Replace lookup for AOCL index with StrongKey

Replace the AOCL leaf index lookup mapping in the wallet database with a
`StrongUtxoKey` lookup mapping. This means that you now need the
addition record and the AOCL leaf index to probe the wallet database for
element inclusion of a monitored UTXO. This is done since this is the way
we do duplication checks in the wallet database -- this is how we probe
if the wallet database already contains the specified UTXO/monitored
UTXO.

This simplifies some logic in the wallet state updating function
`WalletState::update_wallet_state_with_new_block` since we now don't
have to precalculate the potential duplicates -- since the database
logic can handle duplicate-checking internally. Database migration to v2
has been adapted to this schema.

* test: Allow clearing of DbtMap

Allow clearing of DbtMap in tests.

Not added as production code since 1) it is not needed, and 2) I'm not
100 % sure this handling of caching delivers the same thread-safe
guarantees as that of the rest of the code. Warnings added to the
functions so they don't get pulled out from under the test flag
mindlessly.

* trace: log database info in v1->v2 migration test

Also: Add pid value to root data directory path for unit tests -- in
order to ensure that two tests never share any leveldb lock files.

* fix(wallet): Set sync label on wallet-DB recovery if DB is new

If the wallet DB's sync label was never set, that means that the wallet
DB is new. In that case, all possible membership proof recoveries have
been performed when restoring the wallet database from an archival
mutator set. If that's the case, the sync label should be set.

Also: Change logic for when a wallet database is considered new to take
the wallet DB version into account.

* test(wallet): Fix migration test of real v1 wallet-db

CI causing some hickups here. For some reason CI machines consider the
wallet database to be new.

* feat(DbtMap): Add `clear` method

Add `clear` method to `DbtMap` that deletes all entries in the map. Is
added because we want the option of deleting all expected MUTXOs, and we
want a lookup from addition records to expected UTXOs for fast indexing,
and if we clear the list of expected UTXOs, we need to also clear the
lookup map.

* test(DbtVec): Harden some tests of `clear` and `persist`

Ensure that the `len` value reported is correct in these situations.

* fix(wallet): reload tables after migration

Since the `WalletTables` type contains ephemeral data, like cached
vector lengths, it must be reloaded after a database migration since
this cached data will otherwise be retained from the old (pre-migration)
database. Specifically if the length of a persisted vector changes in
the migration process (e.g. due to deduplication), the ephemeral
(non-persisted, living in RAM) length must be updated.

* perf(wallet): Avoid linear search through all expected UTXOs

Add an index to the wallet database allowing for fast lookup of addition
record -> expected UTXO. This avoids the looping over all expected UTXOs
when scanning for matches between expected UTXOs and addition records
during `WalletState::update_wallet_state_with_new_block`, in the method
`WalletState::scan_for_expected_utxos`.

This adds an extra step to the v1 -> v2 wallet migration function
`v1_to_v2::migrate`, since all expected UTXOs must be looped over and
added to the index. In this process we also eliminate potential
duplicates (two expected UTXOs with same addition record) by clearing
the expected UTXO list and rebuilding it. Since the v2 version of the
wallet database has not yet been merged into master, we don't need a
database version yet, as we can simply expand on v2.

* feat: Compute and show max, circulating, and burned supply

Add functions for computing:
 - The amount mined between the genesis block and a given block height.
 - The circulating supply.
 - The max supply.
 - The burned supply.

The last method relies on another, `known_burns` which lists hints about where
to find burned coins. These hints are validated against the archival state, so
even if there is a deep reorganization invalidating some of these hints, there
is no possibility for showing the wrong number.

The function `known_burns` currently only contains valid information for the one
big burn at 16999/3 where 1'526'640 coins were burned. The information for
validating the other two known burns is, for now, missing.

Also: add CLI commands and RPC support for querying the burned, circulating, and
max supplies.

* refactor: Add missing data on burn

* feat: Add JSON-RPC endpoints for supply methods

* fix(JSON-RPC): Adhere to interface standard for accessor methods

Also: document interface standard (accessor special case) in
`json_rpc/README.md`.

* fix(JSON-RPC): Derive tuple serializer/deserializer for requests

Also: mention in `json_rpc/README.md`.

* style: Clarify function contract of `process_inputs_and_outputs_maintain_mps`

Change docstring:
 - Elaborate description.
 - Add effects.
 - Add return values.
 - Add panic cases.

Change function signature: `Err(_)` variant can never be returned, so no need
to wrap the return value in a `Result`. This feature percolates upstream. Fewer
`unwraps()`s yay!

* log: Inform miners about guesser fee of new tips

If the node is composing, or if it is guessing, add an extra info! log message
containing the block subsidy, guesser fee, and the guesser fee percentage
(relative to the block subsidy).

* log(mempool): Inform user about tx-notification sent after Updates

* test: Fix test; avoid overlapping memory objects

Test `hash_removal_record_index_sets::positive_pbt_5` fails occasionally due one
memory object overwriting another. This commit fixes the error by ensuring the
lengths of the memory objects are known when sampling pointers.

* ci: Merge PR #825: Use clang instead of gcc and other fixes

* ci: Use clang instead of gcc

Github CI for ubuntu kept throwing an error related to gcc not understanding
flag `-Wthread-safety` when building leveldb. This change instructs the runner
to exchange gcc for claing, which does know about this flag.

* ci: Skip tests that need large disk space

CI on GitHub seems to be failing for lack of disk space. This commit disables CI
for tests that are known to require a lot of disk space, namely sync loop tests.

* ci(no_lock_build): install and use clang for Linux/macOS builds

Install clang on the Ubuntu runner, print compiler diagnostics, and set CC=clang
/ CXX=clang++ for non-Windows jobs. This prevents GCC from rejecting the
Clang-only -Wthread-safety flag during the leveldb native build while leaving
Windows unchanged.

See CI run:
https://github.com/Neptune-Crypto/neptune-core/actions/runs/20537481523/job/58997577508
and workflow:
.github/workflows/no_lock_build.yml@30e346d01095bf0118169c42b4845b650f6f8b7d

Co-authored-by: GitHub CoPilot <no-reply@example.com>

* ci: Drop problem-maker coverage.yml

* fix: Disable true claims cache for false proof test

Co-authored-by: GitHub CoPilot <no-reply@example.com>

* fix(sync): Tailor temp directory to user

Multiple users cannot simultaneously sync on the same machine, because they both
get a subdirectory in `/tmp/rapid-block-download/` but the first user to create
it gets the access rights, blocking out the second.

This commit fixes that problem by including the username in the directory root.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>

* fix(sync-loop): Abort sync loop (if any) on graceful shutdown

Add a new main-loop to sync-loop message, `Abort` which triggers an immediate
shutdown of the sync loop. This message is sent from the `graceful_shutdown`
function which is responsible for stopping all running tasks.

Previously the sync loop could keep running, even after Ctrl-C and even after
the main loop had shut down. This commit fixes that behavior.

* feat(archival_state): Add UTXO index

Add a UTXO index for fast wallet syncing over many blocks. This new
index holds information about announcements, and outputs of a block.
Together, this index allows for the efficient syncing of wallets
without having to load all blocks from disk, as reading from the
databases is up to 1000x faster than loading the entire block into
memory and fetching the relevant informatino.

The index does not and should not include addition records in a block,
since all addition records are present as leaves in the archival
mutator set, which is assumed to be part of the state of anyone
maintaining a UTXO index.

The UTXO index maintanence is activated by starting the node with the
`--utxo-index` flag.

This commit adds a `rescan_announced_incoming` method to global state
that allows for the scanning of announced transactions only.

* feat(wallet): Allow rescanning for outgoing/spent UTXOs

Use the newly introduced UTXO index to scan the blockchain history for
UTXO expenditures, monitored UTXOs that have been spent.

* feat(wallet): Rescan for all expected UTXOs

Add feature to rescan a specified range of blocks for all expected
UTXOs. Like the two related methods `rescan_announced_incoming` and
`rescan_outgoing`, this method only works on nodes that maintain a UTXO
index.

The new method `rescan_expected_incoming` never loads the entire block
from disk, so performance should be very good. No benchmarks yet though.

* feat(wallet): Rescan for guesser rewards

Rescan a specified range of blocks for guesser rewards going to the
node's wallet.

Like all "rescan" functions, the focus is on performance.

* test(wallet): More tests of rescan

Verify that rescan is idempotent, that it registers guesser rewards
correctly, and that expected balance is restored.

* docs: Add documentation of wallet rescan and UTXO index

* neptune-cli: Add support for wallet rescan

Also add endpoints for tarpc server.

* feat: Add wallet rescanning endpoints to JSON RPC server

Since the RPC server cannot mutate global state, the implied mutation
must be handled as a message from the RPC server to the main loop.

This function holds a write-lock over global state, so many things will
not work while it is running. Maybe that's OK though, since these are
endpoints that will be called very rarely.

* docs(utxo_index): Fix docstrings and var names

Among others: Remove comment that ordering of announcement flags in UTXO
index matches that of the block. This is not true since announcements of
lengths shorter than 2 do not have announcement flags. So some
announcements may be skipped in the index.

* test(utxo_index): Verify behavior of block indexing method

Verify idempotency and the correct handling of short announcements.

* fix(utxo_index): Persist UTXO index state in flush

Add missing call to `persist` to write the changes to the UTXO index
database.

* trace(utxo_index): Adjust some log messages

* chore: Update deprecated TASXM benchmark results

Since randomness was changed in
30e346d01095bf0118169c42b4845b650f6f8b7d, this gives sligthly different
benchmarking results for the hashing of removal records.

* bench(wallet): Add benchmark of wallet rescanning

Add a benhcmark for the rescanning of incoming UTXOs, both announced and
expected. Very happy with the results here, for the rescanning of 110
blocks, of which 10 contain 10 UTXOs for wallet each. With 200 keys
present in wallet.

This benchmark shows that the rescanning times will likely stay under
one minute for the foreseeable future, for all nodes, including mining
pools and exchanges.

wallet_rescan                            fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ rescan                                              │               │               │               │         │
   ├─ rescan_110_blocks_announced_bench  119.8 ms      │ 168.2 ms      │ 131.6 ms      │ 134.6 ms      │ 10      │ 10
   ├─ rescan_110_blocks_expected_bench   37.08 ms      │ 41.9 ms       │ 37.98 ms      │ 38.37 ms      │ 10      │ 10
   ╰─ rescan_110_blocks_outgoing_bench   843.9 µs      │ 1.552 ms      │ 889.6 µs      │ 1.037 ms      │ 10      │ 10

* chore: Format

* refactor(wallet): Move `AnnouncementFlag` definition to own module

* refactor(archival_state): Pass cli args to constructor

Take all of CLI arguments in constructor for the archival state, not
just the network as was done previously. This allows for more
fine-grained control over the initialization, meaning that the UTXO
index is only populated with the genesis block if the node is started
with the `--utxo-index` flag set.

I wanted to also make the `utxo_index` field of `AchivalState` Option<T>
but I ran into multiple borrow checker errors when doing this, and I
ended up using `unwrap()` so much that the total number of lines grew.
If someone comes up with an elegant solution for this though, that would
be a welcome change.

* feat(utxo_index): Get block digests from announcement flags

Add a new lookup table to the UTXO index containing the block hashes of
blocks that have announcements matching the specified
`AnnouncementFlag`. This lookup maps announcement flags to digest such
that e.g. wallet software can quickly know which blocks are relevant for
their keys/addresses.

Values in this lookup table are of type `Vec<Digest>` which is scanned
for duplicates and thus kept in memory at the time of insertion/update.
So to prevent annoying DOS attacks, the length of this list is capped at
10.000. Any wallet receiving incoming UTXOs in more than 10.000 blocks
is expected to maintain its own index and use other methods than this
lookup to restore the wallet.

This is a admittedly a gotcha. But the performance degradation for UTXO
index maintaining nodes if this list can grow forever is not deemed
acceptable, and for the *vast* majority of wallets and application this
cap is irrelevant. And if it becomes a problem, this constant can just
be increased or be made configurable.

* feat(tarpc): Get block hashes from announcement flags

Add an endpoint to return all block digests for blocks that contain any
of the specified announcement flags. Can be used by wallet software to
quickly find blocks that relevant to scan.

* docs: Fix link to const by making `rusty_utxo_index` mod public

Fix link to `MAX_BLOCK_COUNT_FOR_ANNOUNCEMENT_FLAGS` by making the
module in which it is defined public. This constant should be exposed in
the public API as it affects the behavior of functions that use the
`blocks_by_announcement_flag` lookup table.

* fix(utxo_index): Sort announcement flags to make index deterministic

Strictly speaking, this is only required for a test to pass. But the
function advertises that it is idempotent, so this sorting is needed to
guarantee that property.

* feat(rpc_server): Add announcement flag -> block hashes endpoint

Add a new namespace `UtxoIndex` to the RPC server. For now, this
namespace only contains one endpoint: `block_hashes_by_flags`. This new
endpoint returns the block digests for block containing announcements
matching the specified announcement flags. Can be used by 3rd party
wallets for very quick block scans.

* style: Reviewer feedback

Cf. https://github.com/Neptune-Crypto/neptune-core/pull/821

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* log: Avoid scaring user with warning of safety

When calling RPC method `BroadcastMempoolTransactions` on a mempool containing
`PrimitiveWitness`-backed transactions, warnings would be emitted informing the
user that the operation (broadcasting the transaction) could not be performed
because that would leak secret keys.

This commit continues early in the loop such that the conversion from
`PrimitiveWitness`-backed transaction to `TransactionNotification` is never even
attempted -- because we know beforehand that it will fail. Instead, log a debug!
message to explain what's going on. Much less worrisome to the user who is not
interested in the noise.

* trace(mining): Fix punctuation

* test: Fix false negative in test

With a probability of 1/1024, the `translation` value becomes 0, making
this test fail. Solution: disallow the value of 0.

* feat(JSON-RPC): Add mempool endpoints (#823)

* feat(JSON-RPC): Add mempool endpoints

* style: Integrate reviewer feedback

* feat(utxo_index): Allow for more fast lookups

Allow for addition record to block height lookup, and hash(abs index
set) to block height lookup. Also changes data type from Vec<Digest> to
Vec<BlockHeight> for some lookup values in order to save disk space.

Cf. #832.

* refactor(utxo_index): Avoid reliance on storage schema construction

Just use the more low-level NeptuneLevelDb abstraction for the UTXO
index, since this takes up less space, as each key is only stored once
and not twice as is the case when you're using DbtMap.

* refactor(utxo_index): Also index guesser rewards outputs

For completeness, the UTXO index now also maintains guesser rewards
addition records. You could argue that they the mapping addition record
-> block height for guesser reward addition records is not needed but
excluding them feels like a footgun that we might as well avoid.

* test(utxo_index): Test all indexing functions

* feat(utxo_index): Add endpoints for finding relevant blocks

Add two endpoints for light clients/wallets for locating relevant blocks
for their wallets or smart contracts:
  - `utxo_index_block_heights_by_addition_records`
  - `utxo_index_block_heights_by_absolute_index_sets`

These two new endpoints return block heights of (guaranteed canonical)
blocks containing either the requested addition records or the requested
absolute index sets. Unlike the endpoint that returns blocks matching
announcement flags, these return values are guaranteed to reference
canonical blocks. The reason for this distinction is that in the case of
announcement flags you *have* to scan the blocks regardless of the
canonicity of the response since you cannot know *what* the
announcements contain and if they have a matching addition record.

TODO: Test the new endpoints/helper functions added to `ArchivalState`.

Cf. #832.

* docs: Fix wrong docstring on sender randomness derivation

* test(utxo_index): Verify properties of addition_records_to_block_height

* style: Make linter happy

* refactor(archival_state): Improve state-update encapsulation

Make a few state-update related methods in `ArchivalState` private.
Caller should only ever have to interact with
`ArchivalState::set_new_tip`.

* test(utxo_index): Test abs index set -> block height mapper

Verify correct handling of non-canonical/canonical blocks.

test(utxo_index): Ensure no panic when index is not maintained

Verify that an error is returned.

Cf. #833.

* refactor(rpc): Use Rpc type for announcement flag

Also make fields public. Allows for construction of `AnnouncementFlag`
values in downstream dependencies.

Cf. #833.

* docs(utxo_index): Elaborate on UTXO index after redesign

* Nix packages and reproducible dev shell using flake (#835)

* Add `flake.nix` with nix packages and reproducible development shell

* .gitignore: add `result` and `.direnv`

* include `.envrc` file for automatically entering the dev shell with `direnv`

* flake: working `nix run`

* flake: include package `neptune-dashboard`

* flake: use stable rust

* update `README.md` with nix instructions

* nix: simplify `flake.nix`

* Improve nix instructions in `README.md`

* README: remove backtick

* feat(rpc): Check if absolute index set was applied

From the absolute index set, an archival node can check if this set was
already applied.

This commit adds minor functionality to `ArchivalMutatorSet` to achieve
this. This new functionality can be used to speed up wallet operations
on archival nodes, cf. #834.

* feat(rpc): Add tx-priority-related mempool endpoints

Add three endpoints to the `Mempool` namespace:
  - `mempool_getTransactionsByAdditionRecords`
  - `mempool_getTransactionsByAbsoluteIndexSets`
  - `mempool_bestTransactionForNextBlock`

These endpoints can be used by wallets and clients that don't maintain
their own mempool to track where transactions relevant to them are in
the mempool, and what fee they'd need to pay to get a transaction
included in a shortly upcoming block.

This closes #832.

* feat: libp2p Network Stack

Squash and merge of PR #828: https://github.com/Neptune-Crypto/neptune-core/pull/828.

This commit introduces the libp2p network stack. Libp2p is a library for peer-to-peer
protocols such as blockchains. It comes with a variety of off-the-shelf subprotocols
for things like NAT traversal and peer discovery. This PR activates a libp2p network
stack while reusing the same old battle-tested blockchain peer loop. For the time
being, the legacy network stack's control flow pathways into the blockchain peer
loop (specifically, those contained in connect_to_peers.rs) remain active, but going
forward these are considered deprecated and scheduled for removal.

The new module is documented verbosely in the documentation under
`neptune_core/network_stack.md`.

* docs(README.md): Update

Instructions for starting `neptune-core` have changed: now `--peer` is optional.

* chore: Silence clippy

Fix docstrings, remove ampersands, mark `pub(crate)`, remove unused, etc.

* chore: Move unused test helper functions to test module

The moved code snippets are typically generators of `Strategy`s for various
structs. They are only used in tests, so there is no need to expose them
outside of the test environment.

* feat: Consolidate UTXOs

Add an API endpoint for creating and broadcasting a transaction that
consolidates UTXOs. Call this endpoint automatically if the
corresponding CLI flag is set and when a new block arrives from a peer.

Current implementation is somewhat primitive: The created consolidation
transaction always takes four UTXOs as input and outputs one. Also the
fee is currently hardcoded.

Also: add CLI argument for specifying auto-consolidate setting

Also add `second_parse` method on `cli_args::Args` to verify internal
consistency with respect to interrelated fields (e.g. fields dependent
on `network` which because of those dependencies cannot be parse through
mere CLAP). This new function also populates cache so that the accessor
methods have something to return. Current cached values are: tx proving
capability, and auto consolidation settings.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* fix(network): Don't use protocol in `PeerScore`

Instead of using protocol agreement in the calculation of
`PeerScore`, we reject any connections if the two peers do not agree on
the protocol.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* fork!: Upgrade Triton VM to v2.0.0, with checkpoint

1. Upgrade dependency tasm-lib to v2.0.0 to get the Triton VM v2.0.0 which
fixes a soundness error in Triton VM's proof system.

2. `block-claims`

Add a new binary `block-claims` that reads the archival block database and
outputs a list of block validity claims for blocks 0..=arg. This binary was
used for (3).

3. `checkpoint.dat`

Add new file `assets/checkpoint.dat` which contains the block validity claims
for blocks 0..=23400.

4. Accept checkpoint

Call and add function in archival state initializer, called `accept_checkpoint`,
which reads static program memory macroically guaranteed to agree with
`checkpoint.dat`, parses it as a list of claims, and populates the true claims
cache with these claims.

5. Untest true claims cache

Up until now, the true claims cache was used only for tests. Going forward it is
going to be used in production as well, so remove the test flag.

6. Style: IntoIterator

Modify `cache_true_claim` --> `cache_true_claims` to take a list (technically, a
`IntoIterator<_>` object) of `Claim`s rather than a single Claim. Avoid taking
the lock every time when there are some 23401 claims to cache, in favor of
taking it only once.

7. `pub`

Mark `pub` functions, structs, and fields that are used in binary
`block-claims`.

8. Define consensus rule set for sound version of Triton VM

test: Use tvm proof version 1 for all tests

This is done to prevent the need of two different versions of Triton VM.
With this change only version 1, which is assumed to be sound, is used.
So version 0 is avoided since it is unsound and we can avoid its
dependency in neptune-core.

This ensures that production and test behavior are somewhat similar:
they only deviate on Triton VM versions, not on any other consensus
logic (except for size of memory requirements in the PoW).

This means that existing tests verifying transitions across hardfork
boundaries can keep working and keep being meaningful. Example: The
test in `peer_loop`: `hardfork_alpha_happy_case`.

Introduces a new `enum` `TritonProofVersion` such the the distiction on
the upstream Triton VM version can be better isolated.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* refactor(peer-loop): Validate sync block proof prior to relay

Previously, sync blocks (individual blocks requested by height,
typically by the sync loop) were forwarded to the sync loop with zero
validation, exposing the node to a potential denial-of-service attack.
This commit makes that attack more expensive: now the incoming block's
proof is validated, so the attacker does still need to include valid
block proofs (and appendices).

It is not possible to validate the entire block because that relation
pertains to the block's predecessor which is not available in this
context.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* refactor: Default consensus rule set to TVM proof version 1

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* feat(block_claims): Dump testnet block claims and add checkpoint for testnet

This allows us to test that the recently implemented upgrade of the
Triton VM version to proof version 1 works on testnet also, that the
transition from proof version 0 to proof version 1 is smooth.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* chore: Make linter happy

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* test: Fix tests related to hard fork tvm proof v1

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* feat: Roll back to checkpoint if current tip is not valid

On startup, check the block program proof for the current tip. If it does not
pass validation, then roll back to the checkpoint.

Ensure we don't panic if rollback luca block is not known; handle rollback of
block correctly in case the threshold/luca block, the latest block valid
under the old version of Triton VM, is not known.

Co-authored-by: Thorkil Schmidiger <thor@neptune.cash>

* fix(mine_loop): Off-by-one error in coinbase production

Set correct consensus rule set when creating the coinbase transaction.
Prior to this commit, there was an off-by-one error here which had never
been caught because the consensus rule set for transactions (and even
that of block validity) had never changed through a hardfork.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* test: Ignore invalid blocks in block import when TVM proof version is 0

Since the "true claims" is populated with only canonical blocks prior to
block height 23401, block imports from a downloaded list of blocks
cannot handle reorganizations, orphaned blocks, in these old blocks,
unless we simply choose to ignore invalid blocks prior to this block
height. Since the invalid blocks are never applied to the state update
function and since we now that they are not canonical, this is fine.

* chore: reorder imports

* devops: Don't use dedicated build target directory in Makefile

By deleting this env varibale in Makefile, the commands run through
`make` now use the same target directory as those run through `cargo
<{build,test,clippy,fmt,...}>` commands. This saves disk space and
compilation time for users that use both ways of
compiling/testing/linting.

* devops: Force use of clang compiler bc leveldb-sys

For some reason, leveldb-sys is failing to build with gcc/gcc++. This
happens because the cmake build script used by this crate sets the flag
'-Wthread-safety' which gcc does not know. Resulting in this error:
`c++: error: unrecognized command-line option '-Wthread-safety'`

A fix is to switch to clang and download a package that contains a
binary for `lstdc++`, otherwise linking in clang fails with the error
`"/usr/bin/ld: cannot find -lstdc++: No such file or directory"`.

build(Makefile): Verify C++ toolchain before compilation

Add two recipes to the Makefile, one for checking presence of clang/clang++, and
one for installing it (if on Ubuntu/Debian) or exiting gracefully.
Simultaneously with clang/clang++, libstdc++14-dev is installed as well, but
presumably any version of C++ can work.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* devops: Remove unused dependency

* chore: cargo update

* security: Upgrade to ratatui 0.30

ratatui 0.29 depended on an unsafe version of lru. Cf.:
https://github.com/Neptune-Crypto/neptune-core/security/dependabot/14
and
https://cwe.mitre.org/data/definitions/476.html

From dependabot:
Package lru (Rust)
Affected versions
>= 0.9.0, < 0.16.3
Patched version
0.16.3

* fix: clippy rule name

`needless_if` was renamed to `needless_ifs`.

* chore: Major-version upgrade some dependencies

Upgrade a few dependencies such that the code base only depends on one
version of itertools, not three as was previously the case.

* trace: Fine-grained timing when setting new tip

Add some debug log messages when a new block is applied to the state.
This allows us to pinpoint which parts of the state update that is slow.

I know that it's the wallet that's currently causing problems for nodes
that manage many UTXOs, but I want more information about whether it's
the application of a block or the restoration of MS membership proofs
(likely the culprit).

* chore: Update release workflow files

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* fix(handshake): Use correct version-compatibility checker

Ensure that legacy peer-to-peer stack agrees with libp2p stack on
version compatibility check.

Don't allow connections between v0.5.0 and v0.6.0.

Also: Fix bug shared handshake validator, on `HandshakeData`, relating
to versions.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* chore: Prepare new crates for publishing

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* docs(`releasing.md`): Specify where to find versions to bump

Avoid wasting time next time to find bumpable versions.

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

* chore: Release v0.6.0

Co-authored-by: Alan Szepieniec <alan@neptune.cash>

---------

Co-authored-by: KaffinPX <73744616+KaffinPX@users.noreply.github.com>
Co-authored-by: Alan Szepieniec <alan@neptune.cash>
Co-authored-by: sword_smith <thor@neptune.cash>
Co-authored-by: KaffinPX <KaffinPX@gmail.com>
Co-authored-by: Jan Ferdinand Sauer <ferdinand@neptune.cash>
Co-authored-by: Sergey Kaunov <skaunov@disroot.org>
Co-authored-by: GitHub CoPilot <no-reply@example.com>
Co-authored-by: Thorkil Værge <thv@neptune.cash>
Co-authored-by: Mathis Wellmann <wellmannmathis@gmail.com>
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 this pull request may close these issues.

2 participants