Skip to content

Commit e64342a

Browse files
authoredFeb 5, 2024
Merge pull request #2810 from TheBlueMatt/2023-12-arbitrary-fuzz-config
Update `full_stack_target` to take an arbitrary config object
2 parents e594021 + 7377cc9 commit e64342a

31 files changed

+368
-202
lines changed
 

‎.github/workflows/build.yml

+6-2
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ jobs:
110110
run: |
111111
cd lightning
112112
RUSTFLAGS="--cfg=require_route_graph_test" cargo test
113-
RUSTFLAGS="--cfg=require_route_graph_test" cargo test --features hashbrown
113+
RUSTFLAGS="--cfg=require_route_graph_test" cargo test --features hashbrown,ahash
114114
cd ..
115115
- name: Run benchmarks on Rust ${{ matrix.toolchain }}
116116
run: |
@@ -176,7 +176,7 @@ jobs:
176176
fuzz:
177177
runs-on: ubuntu-latest
178178
env:
179-
TOOLCHAIN: 1.58
179+
TOOLCHAIN: 1.63
180180
steps:
181181
- name: Checkout source code
182182
uses: actions/checkout@v3
@@ -188,6 +188,10 @@ jobs:
188188
run: |
189189
sudo apt-get update
190190
sudo apt-get -y install build-essential binutils-dev libunwind-dev
191+
- name: Pin the regex dependency
192+
run: |
193+
cd fuzz && cargo update -p regex --precise "1.9.6" --verbose && cd ..
194+
cd lightning-invoice/fuzz && cargo update -p regex --precise "1.9.6" --verbose
191195
- name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }}
192196
run: cd fuzz && RUSTFLAGS="--cfg=fuzzing" cargo test --verbose --color always
193197
- name: Run fuzzers

‎bench/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ name = "bench"
99
harness = false
1010

1111
[features]
12-
hashbrown = ["lightning/hashbrown"]
12+
hashbrown = ["lightning/hashbrown", "lightning/ahash"]
1313

1414
[dependencies]
1515
lightning = { path = "../lightning", features = ["_test_utils", "criterion"] }

‎ci/check-cfg-flags.py

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ def check_feature(feature):
1313
pass
1414
elif feature == "no-std":
1515
pass
16+
elif feature == "ahash":
17+
pass
1618
elif feature == "hashbrown":
1719
pass
1820
elif feature == "backtrace":

‎fuzz/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ lightning = { path = "../lightning", features = ["regex", "hashbrown", "_test_ut
2222
lightning-rapid-gossip-sync = { path = "../lightning-rapid-gossip-sync" }
2323
bitcoin = { version = "0.30.2", features = ["secp-lowmemory"] }
2424
hex = { package = "hex-conservative", version = "0.1.1", default-features = false }
25-
hashbrown = "0.8"
25+
hashbrown = "0.13"
2626

2727
afl = { version = "0.12", optional = true }
2828
honggfuzz = { version = "0.5", optional = true, default-features = false }

‎fuzz/src/full_stack.rs

+17-30
Large diffs are not rendered by default.

‎fuzz/src/peer_crypt.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ pub fn do_test(data: &[u8]) {
8383
Ok(len) => len,
8484
Err(_) => return,
8585
};
86-
buf.copy_from_slice(&get_slice!(len as usize + 16));
86+
buf[..len as usize + 16].copy_from_slice(&get_slice!(len as usize + 16));
8787
match crypter.decrypt_message(&mut buf[..len as usize + 16]) {
8888
Ok(_) => {},
8989
Err(_) => return,

‎lightning-invoice/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ bech32 = { version = "0.9.0", default-features = false }
2424
lightning = { version = "0.0.121", path = "../lightning", default-features = false }
2525
secp256k1 = { version = "0.27.0", default-features = false, features = ["recovery", "alloc"] }
2626
num-traits = { version = "0.2.8", default-features = false }
27-
hashbrown = { version = "0.8", optional = true }
27+
hashbrown = { version = "0.13", optional = true }
2828
serde = { version = "1.0.118", optional = true }
2929
bitcoin = { version = "0.30.2", default-features = false }
3030

‎lightning/Cargo.toml

+13-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ unsafe_revoked_tx_signing = []
3131
# Override signing to not include randomness when generating signatures for test vectors.
3232
_test_vectors = []
3333

34-
no-std = ["hashbrown", "bitcoin/no-std", "core2/alloc", "libm"]
34+
no-std = ["hashbrown", "ahash", "bitcoin/no-std", "core2/alloc", "libm"]
3535
std = ["bitcoin/std"]
3636

3737
# Generates low-r bitcoin signatures, which saves 1 byte in 50% of the cases
@@ -42,14 +42,25 @@ default = ["std", "grind_signatures"]
4242
[dependencies]
4343
bitcoin = { version = "0.30.2", default-features = false, features = ["secp-recovery"] }
4444

45-
hashbrown = { version = "0.8", optional = true }
45+
hashbrown = { version = "0.13", optional = true }
46+
ahash = { version = "0.8", optional = true, default-features = false }
4647
hex = { package = "hex-conservative", version = "0.1.1", default-features = false }
4748
regex = { version = "1.5.6", optional = true }
4849
backtrace = { version = "0.3", optional = true }
4950

5051
core2 = { version = "0.3.0", optional = true, default-features = false }
5152
libm = { version = "0.2", optional = true, default-features = false }
5253

54+
# Because ahash no longer (kinda poorly) does it for us, (roughly) list out the targets that
55+
# getrandom supports and turn on ahash's `runtime-rng` feature for them.
56+
[target.'cfg(not(any(target_os = "unknown", target_os = "none")))'.dependencies]
57+
ahash = { version = "0.8", optional = true, default-features = false, features = ["runtime-rng"] }
58+
59+
# Not sure what target_os gets set to for sgx, so to be safe always enable runtime-rng for x86_64
60+
# platforms (assuming LDK isn't being used on embedded x86-64 running directly on metal).
61+
[target.'cfg(target_arch = "x86_64")'.dependencies]
62+
ahash = { version = "0.8", optional = true, default-features = false, features = ["runtime-rng"] }
63+
5364
[dev-dependencies]
5465
regex = "1.5.6"
5566

‎lightning/src/chain/chainmonitor.rs

+4-5
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ use crate::ln::channelmanager::ChannelDetails;
4343

4444
use crate::prelude::*;
4545
use crate::sync::{RwLock, RwLockReadGuard, Mutex, MutexGuard};
46-
use core::iter::FromIterator;
4746
use core::ops::Deref;
4847
use core::sync::atomic::{AtomicUsize, Ordering};
4948
use bitcoin::secp256k1::PublicKey;
@@ -318,7 +317,7 @@ where C::Target: chain::Filter,
318317
FN: Fn(&ChannelMonitor<ChannelSigner>, &TransactionData) -> Vec<TransactionOutputs>
319318
{
320319
let err_str = "ChannelMonitor[Update] persistence failed unrecoverably. This indicates we cannot continue normal operation and must shut down.";
321-
let funding_outpoints: HashSet<OutPoint> = HashSet::from_iter(self.monitors.read().unwrap().keys().cloned());
320+
let funding_outpoints = hash_set_from_iter(self.monitors.read().unwrap().keys().cloned());
322321
for funding_outpoint in funding_outpoints.iter() {
323322
let monitor_lock = self.monitors.read().unwrap();
324323
if let Some(monitor_state) = monitor_lock.get(funding_outpoint) {
@@ -420,7 +419,7 @@ where C::Target: chain::Filter,
420419
/// transactions relevant to the watched channels.
421420
pub fn new(chain_source: Option<C>, broadcaster: T, logger: L, feeest: F, persister: P) -> Self {
422421
Self {
423-
monitors: RwLock::new(HashMap::new()),
422+
monitors: RwLock::new(new_hash_map()),
424423
sync_persistence_id: AtomicCounter::new(),
425424
chain_source,
426425
broadcaster,
@@ -486,9 +485,9 @@ where C::Target: chain::Filter,
486485
#[cfg(not(c_bindings))]
487486
/// Lists the pending updates for each [`ChannelMonitor`] (by `OutPoint` being monitored).
488487
pub fn list_pending_monitor_updates(&self) -> HashMap<OutPoint, Vec<MonitorUpdateId>> {
489-
self.monitors.read().unwrap().iter().map(|(outpoint, holder)| {
488+
hash_map_from_iter(self.monitors.read().unwrap().iter().map(|(outpoint, holder)| {
490489
(*outpoint, holder.pending_monitor_updates.lock().unwrap().clone())
491-
}).collect()
490+
}))
492491
}
493492

494493
#[cfg(c_bindings)]

‎lightning/src/chain/channelmonitor.rs

+21-16
Original file line numberDiff line numberDiff line change
@@ -1235,7 +1235,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
12351235
channel_parameters.clone(), initial_holder_commitment_tx, secp_ctx
12361236
);
12371237

1238-
let mut outputs_to_watch = HashMap::new();
1238+
let mut outputs_to_watch = new_hash_map();
12391239
outputs_to_watch.insert(funding_info.0.txid, vec![(funding_info.0.index as u32, funding_info.1.clone())]);
12401240

12411241
Self::from_impl(ChannelMonitorImpl {
@@ -1262,17 +1262,17 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
12621262
on_holder_tx_csv: counterparty_channel_parameters.selected_contest_delay,
12631263

12641264
commitment_secrets: CounterpartyCommitmentSecrets::new(),
1265-
counterparty_claimable_outpoints: HashMap::new(),
1266-
counterparty_commitment_txn_on_chain: HashMap::new(),
1267-
counterparty_hash_commitment_number: HashMap::new(),
1268-
counterparty_fulfilled_htlcs: HashMap::new(),
1265+
counterparty_claimable_outpoints: new_hash_map(),
1266+
counterparty_commitment_txn_on_chain: new_hash_map(),
1267+
counterparty_hash_commitment_number: new_hash_map(),
1268+
counterparty_fulfilled_htlcs: new_hash_map(),
12691269

12701270
prev_holder_signed_commitment_tx: None,
12711271
current_holder_commitment_tx: holder_commitment_tx,
12721272
current_counterparty_commitment_number: 1 << 48,
12731273
current_holder_commitment_number,
12741274

1275-
payment_preimages: HashMap::new(),
1275+
payment_preimages: new_hash_map(),
12761276
pending_monitor_events: Vec::new(),
12771277
pending_events: Vec::new(),
12781278
is_processing_pending_events: false,
@@ -2174,7 +2174,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
21742174
/// HTLCs which were resolved on-chain (i.e. where the final HTLC resolution was done by an
21752175
/// event from this `ChannelMonitor`).
21762176
pub(crate) fn get_all_current_outbound_htlcs(&self) -> HashMap<HTLCSource, (HTLCOutputInCommitment, Option<PaymentPreimage>)> {
2177-
let mut res = HashMap::new();
2177+
let mut res = new_hash_map();
21782178
// Just examine the available counterparty commitment transactions. See docs on
21792179
// `fail_unbroadcast_htlcs`, below, for justification.
21802180
let us = self.inner.lock().unwrap();
@@ -2226,7 +2226,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitor<Signer> {
22262226
return self.get_all_current_outbound_htlcs();
22272227
}
22282228

2229-
let mut res = HashMap::new();
2229+
let mut res = new_hash_map();
22302230
macro_rules! walk_htlcs {
22312231
($holder_commitment: expr, $htlc_iter: expr) => {
22322232
for (htlc, source) in $htlc_iter {
@@ -3172,7 +3172,11 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
31723172
(htlc, htlc_source.as_ref().map(|htlc_source| htlc_source.as_ref()))
31733173
), logger);
31743174
} else {
3175-
debug_assert!(false, "We should have per-commitment option for any recognized old commitment txn");
3175+
// Our fuzzers aren't contrained by pesky things like valid signatures, so can
3176+
// spend our funding output with a transaction which doesn't match our past
3177+
// commitment transactions. Thus, we can only debug-assert here when not
3178+
// fuzzing.
3179+
debug_assert!(cfg!(fuzzing), "We should have per-commitment option for any recognized old commitment txn");
31763180
fail_unbroadcast_htlcs!(self, "revoked counterparty", commitment_txid, tx, height,
31773181
block_hash, [].iter().map(|reference| *reference), logger);
31783182
}
@@ -3679,6 +3683,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
36793683
claimable_outpoints.append(&mut new_outpoints);
36803684
if new_outpoints.is_empty() {
36813685
if let Some((mut new_outpoints, new_outputs)) = self.check_spend_holder_transaction(&tx, height, &block_hash, &logger) {
3686+
#[cfg(not(fuzzing))]
36823687
debug_assert!(commitment_tx_to_counterparty_output.is_none(),
36833688
"A commitment transaction matched as both a counterparty and local commitment tx?");
36843689
if !new_outputs.1.is_empty() {
@@ -3935,7 +3940,7 @@ impl<Signer: WriteableEcdsaChannelSigner> ChannelMonitorImpl<Signer> {
39353940
/// Filters a block's `txdata` for transactions spending watched outputs or for any child
39363941
/// transactions thereof.
39373942
fn filter_block<'a>(&self, txdata: &TransactionData<'a>) -> Vec<&'a Transaction> {
3938-
let mut matched_txn = HashSet::new();
3943+
let mut matched_txn = new_hash_set();
39393944
txdata.iter().filter(|&&(_, tx)| {
39403945
let mut matches = self.spends_watched_output(tx);
39413946
for input in tx.input.iter() {
@@ -4450,7 +4455,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
44504455
}
44514456

44524457
let counterparty_claimable_outpoints_len: u64 = Readable::read(reader)?;
4453-
let mut counterparty_claimable_outpoints = HashMap::with_capacity(cmp::min(counterparty_claimable_outpoints_len as usize, MAX_ALLOC_SIZE / 64));
4458+
let mut counterparty_claimable_outpoints = hash_map_with_capacity(cmp::min(counterparty_claimable_outpoints_len as usize, MAX_ALLOC_SIZE / 64));
44544459
for _ in 0..counterparty_claimable_outpoints_len {
44554460
let txid: Txid = Readable::read(reader)?;
44564461
let htlcs_count: u64 = Readable::read(reader)?;
@@ -4464,7 +4469,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
44644469
}
44654470

44664471
let counterparty_commitment_txn_on_chain_len: u64 = Readable::read(reader)?;
4467-
let mut counterparty_commitment_txn_on_chain = HashMap::with_capacity(cmp::min(counterparty_commitment_txn_on_chain_len as usize, MAX_ALLOC_SIZE / 32));
4472+
let mut counterparty_commitment_txn_on_chain = hash_map_with_capacity(cmp::min(counterparty_commitment_txn_on_chain_len as usize, MAX_ALLOC_SIZE / 32));
44684473
for _ in 0..counterparty_commitment_txn_on_chain_len {
44694474
let txid: Txid = Readable::read(reader)?;
44704475
let commitment_number = <U48 as Readable>::read(reader)?.0;
@@ -4474,7 +4479,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
44744479
}
44754480

44764481
let counterparty_hash_commitment_number_len: u64 = Readable::read(reader)?;
4477-
let mut counterparty_hash_commitment_number = HashMap::with_capacity(cmp::min(counterparty_hash_commitment_number_len as usize, MAX_ALLOC_SIZE / 32));
4482+
let mut counterparty_hash_commitment_number = hash_map_with_capacity(cmp::min(counterparty_hash_commitment_number_len as usize, MAX_ALLOC_SIZE / 32));
44784483
for _ in 0..counterparty_hash_commitment_number_len {
44794484
let payment_hash: PaymentHash = Readable::read(reader)?;
44804485
let commitment_number = <U48 as Readable>::read(reader)?.0;
@@ -4497,7 +4502,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
44974502
let current_holder_commitment_number = <U48 as Readable>::read(reader)?.0;
44984503

44994504
let payment_preimages_len: u64 = Readable::read(reader)?;
4500-
let mut payment_preimages = HashMap::with_capacity(cmp::min(payment_preimages_len as usize, MAX_ALLOC_SIZE / 32));
4505+
let mut payment_preimages = hash_map_with_capacity(cmp::min(payment_preimages_len as usize, MAX_ALLOC_SIZE / 32));
45014506
for _ in 0..payment_preimages_len {
45024507
let preimage: PaymentPreimage = Readable::read(reader)?;
45034508
let hash = PaymentHash(Sha256::hash(&preimage.0[..]).to_byte_array());
@@ -4537,7 +4542,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
45374542
}
45384543

45394544
let outputs_to_watch_len: u64 = Readable::read(reader)?;
4540-
let mut outputs_to_watch = HashMap::with_capacity(cmp::min(outputs_to_watch_len as usize, MAX_ALLOC_SIZE / (mem::size_of::<Txid>() + mem::size_of::<u32>() + mem::size_of::<Vec<ScriptBuf>>())));
4545+
let mut outputs_to_watch = hash_map_with_capacity(cmp::min(outputs_to_watch_len as usize, MAX_ALLOC_SIZE / (mem::size_of::<Txid>() + mem::size_of::<u32>() + mem::size_of::<Vec<ScriptBuf>>())));
45414546
for _ in 0..outputs_to_watch_len {
45424547
let txid = Readable::read(reader)?;
45434548
let outputs_len: u64 = Readable::read(reader)?;
@@ -4579,7 +4584,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
45794584
let mut counterparty_node_id = None;
45804585
let mut confirmed_commitment_tx_counterparty_output = None;
45814586
let mut spendable_txids_confirmed = Some(Vec::new());
4582-
let mut counterparty_fulfilled_htlcs = Some(HashMap::new());
4587+
let mut counterparty_fulfilled_htlcs = Some(new_hash_map());
45834588
let mut initial_counterparty_commitment_info = None;
45844589
let mut channel_id = None;
45854590
read_tlv_fields!(reader, {

‎lightning/src/chain/onchaintx.rs

+10-8
Original file line numberDiff line numberDiff line change
@@ -374,13 +374,13 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP
374374
signer.provide_channel_parameters(&channel_parameters);
375375

376376
let pending_claim_requests_len: u64 = Readable::read(reader)?;
377-
let mut pending_claim_requests = HashMap::with_capacity(cmp::min(pending_claim_requests_len as usize, MAX_ALLOC_SIZE / 128));
377+
let mut pending_claim_requests = hash_map_with_capacity(cmp::min(pending_claim_requests_len as usize, MAX_ALLOC_SIZE / 128));
378378
for _ in 0..pending_claim_requests_len {
379379
pending_claim_requests.insert(Readable::read(reader)?, Readable::read(reader)?);
380380
}
381381

382382
let claimable_outpoints_len: u64 = Readable::read(reader)?;
383-
let mut claimable_outpoints = HashMap::with_capacity(cmp::min(pending_claim_requests_len as usize, MAX_ALLOC_SIZE / 128));
383+
let mut claimable_outpoints = hash_map_with_capacity(cmp::min(pending_claim_requests_len as usize, MAX_ALLOC_SIZE / 128));
384384
for _ in 0..claimable_outpoints_len {
385385
let outpoint = Readable::read(reader)?;
386386
let ancestor_claim_txid = Readable::read(reader)?;
@@ -445,8 +445,8 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
445445
prev_holder_commitment: None,
446446
signer,
447447
channel_transaction_parameters: channel_parameters,
448-
pending_claim_requests: HashMap::new(),
449-
claimable_outpoints: HashMap::new(),
448+
pending_claim_requests: new_hash_map(),
449+
claimable_outpoints: new_hash_map(),
450450
locktimed_packages: BTreeMap::new(),
451451
onchain_events_awaiting_threshold_conf: Vec::new(),
452452
pending_claim_events: Vec::new(),
@@ -686,7 +686,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
686686
if let Some(claim_id) = claim_id {
687687
if let Some(claim) = self.pending_claim_requests.remove(&claim_id) {
688688
for outpoint in claim.outpoints() {
689-
self.claimable_outpoints.remove(&outpoint);
689+
self.claimable_outpoints.remove(outpoint);
690690
}
691691
}
692692
} else {
@@ -806,7 +806,9 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
806806
claim_id
807807
},
808808
};
809-
debug_assert!(self.pending_claim_requests.get(&claim_id).is_none());
809+
// Because fuzzing can cause hash collisions, we can end up with conflicting claim
810+
// ids here, so we only assert when not fuzzing.
811+
debug_assert!(cfg!(fuzzing) || self.pending_claim_requests.get(&claim_id).is_none());
810812
for k in req.outpoints() {
811813
log_info!(logger, "Registering claiming request for {}:{}", k.txid, k.vout);
812814
self.claimable_outpoints.insert(k.clone(), (claim_id, conf_height));
@@ -832,7 +834,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
832834
F::Target: FeeEstimator,
833835
{
834836
log_debug!(logger, "Updating claims view at height {} with {} matched transactions in block {}", cur_height, txn_matched.len(), conf_height);
835-
let mut bump_candidates = HashMap::new();
837+
let mut bump_candidates = new_hash_map();
836838
for tx in txn_matched {
837839
// Scan all input to verify is one of the outpoint spent is of interest for us
838840
let mut claimed_outputs_material = Vec::new();
@@ -1018,7 +1020,7 @@ impl<ChannelSigner: WriteableEcdsaChannelSigner> OnchainTxHandler<ChannelSigner>
10181020
where B::Target: BroadcasterInterface,
10191021
F::Target: FeeEstimator,
10201022
{
1021-
let mut bump_candidates = HashMap::new();
1023+
let mut bump_candidates = new_hash_map();
10221024
let onchain_events_awaiting_threshold_conf =
10231025
self.onchain_events_awaiting_threshold_conf.drain(..).collect::<Vec<_>>();
10241026
for entry in onchain_events_awaiting_threshold_conf {

‎lightning/src/events/bump_transaction.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ where
391391
/// Returns a new instance backed by the given [`WalletSource`] that serves as an implementation
392392
/// of [`CoinSelectionSource`].
393393
pub fn new(source: W, logger: L) -> Self {
394-
Self { source, logger, locked_utxos: Mutex::new(HashMap::new()) }
394+
Self { source, logger, locked_utxos: Mutex::new(new_hash_map()) }
395395
}
396396

397397
/// Performs coin selection on the set of UTXOs obtained from

0 commit comments

Comments
 (0)
Please sign in to comment.