diff --git a/Cargo.lock b/Cargo.lock index b0762f3..923c3be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1206,6 +1206,16 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.22" @@ -1372,6 +1382,29 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + [[package]] name = "paste" version = "1.0.14" @@ -1508,6 +1541,15 @@ dependencies = [ "getrandom", ] +[[package]] +name = "redox_syscall" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "regex" version = "1.10.4" @@ -1744,6 +1786,7 @@ dependencies = [ "rstest", "rstest_reuse", "serde_yaml", + "serial_test", "strict_encoding", "strict_types", "strum", @@ -1906,6 +1949,15 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +[[package]] +name = "scc" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b13f8ea6177672c49d12ed964cca44836f59621981b04a3e26b87e675181de" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.23" @@ -1915,6 +1967,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sdd" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" + [[package]] name = "secp256k1" version = "0.29.0" @@ -2104,6 +2168,31 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" +dependencies = [ + "futures", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "sha2" version = "0.10.8" diff --git a/Cargo.toml b/Cargo.toml index 313fd58..7140ef0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ rand = "0.8.5" rstest = "0.19.0" rstest_reuse = "0.6.0" serde_yaml = "0.9" +serial_test = "3.2.0" strum = { version = "0.26.2", features = ["derive"] } strum_macros = "0.26.2" time = "0.3.34" diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 197c884..037f580 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,18 +1,44 @@ -version: '3.2' - services: - bitcoind: + bitcoind_1: image: registry.gitlab.com/hashbeam/docker/bitcoind:25.0 profiles: [electrum] command: "-fallbackfee=0.0002" - electrs: + bitcoind_2: + image: registry.gitlab.com/hashbeam/docker/bitcoind:25.0 + profiles: [electrum] + command: "-fallbackfee=0.0002" + bitcoind_3: + image: registry.gitlab.com/hashbeam/docker/bitcoind:25.0 + profiles: [electrum] + command: "-fallbackfee=0.0002" + electrs_1: image: registry.gitlab.com/hashbeam/docker/electrs:0.9.14 profiles: [electrum] + environment: + BTCHOST: bitcoind_1 ports: - 50001:50001 depends_on: - - bitcoind - esplora: + - bitcoind_1 + electrs_2: + image: registry.gitlab.com/hashbeam/docker/electrs:0.9.14 + profiles: [electrum] + environment: + BTCHOST: bitcoind_2 + ports: + - 50002:50001 + depends_on: + - bitcoind_2 + electrs_3: + image: registry.gitlab.com/hashbeam/docker/electrs:0.9.14 + profiles: [electrum] + environment: + BTCHOST: bitcoind_3 + ports: + - 50003:50001 + depends_on: + - bitcoind_3 + esplora_1: image: blockstream/esplora:956c74f42eb6ad803d8aedc272ba83d3aa6dcf5c profiles: [esplora] command: /srv/explorer/run.sh bitcoin-regtest explorer @@ -22,5 +48,29 @@ services: NO_ADDRESS_SEARCH: 1 NO_REGTEST_MINING: 1 ports: - - 50002:50001 + - 50004:50001 - 8094:80 + esplora_2: + image: blockstream/esplora:956c74f42eb6ad803d8aedc272ba83d3aa6dcf5c + profiles: [esplora] + command: /srv/explorer/run.sh bitcoin-regtest explorer + environment: + DEBUG: verbose + NO_PRECACHE: 1 + NO_ADDRESS_SEARCH: 1 + NO_REGTEST_MINING: 1 + ports: + - 50005:50001 + - 8095:80 + esplora_3: + image: blockstream/esplora:956c74f42eb6ad803d8aedc272ba83d3aa6dcf5c + profiles: [esplora] + command: /srv/explorer/run.sh bitcoin-regtest explorer + environment: + DEBUG: verbose + NO_PRECACHE: 1 + NO_ADDRESS_SEARCH: 1 + NO_REGTEST_MINING: 1 + ports: + - 50006:50001 + - 8096:80 diff --git a/tests/start_services.sh b/tests/start_services.sh index 3f6d93b..ff32e6c 100755 --- a/tests/start_services.sh +++ b/tests/start_services.sh @@ -2,67 +2,128 @@ set -eu _die () { - echo "ERR: $*" + echo "ERR: $*" >&2 exit 1 } -COMPOSE_BASE="docker compose" -if ! $COMPOSE_BASE >/dev/null; then - echo "could not call docker compose (hint: install docker compose plugin)" - exit 1 +_prepare_bitcoin_nodes() { + $BCLI_1 createwallet miner + $BCLI_2 createwallet miner + $BCLI_3 createwallet miner + $BCLI_1 -rpcwallet=miner -generate 103 + $BCLI_2 -rpcwallet=miner -generate 103 + # connect the 2 bitcoin services for the reorg + if [ "$PROFILE" == "esplora" ]; then + $BCLI_2 addnode "esplora_3:18444" "onetry" + $BCLI_3 addnode "esplora_2:18444" "onetry" + elif [ "$PROFILE" == "electrum" ]; then + $BCLI_2 addnode "bitcoind_3:18444" "onetry" + $BCLI_3 addnode "bitcoind_2:18444" "onetry" + fi +} + +_wait_for_bitcoind() { + # wait for bitcoind to be up + bitcoind_service_name="$1" + until $COMPOSE logs $bitcoind_service_name |grep -q 'Bound to'; do + sleep 1 + done +} + +_wait_for_electrs() { + # wait for electrs to have completed startup + electrs_service_name="$1" + until $COMPOSE logs $electrs_service_name |grep -q 'finished full compaction'; do + sleep 1 + done +} + +_wait_for_esplora() { + # wait for esplora to have completed startup + esplora_service_name="$1" + until $COMPOSE logs $esplora_service_name |grep -q 'run: nginx:'; do + sleep 1 + done +} + +_stop_esplora() { + # stop an esplora sub service + esplora_service_name="$1" + esplora_sub_service_name="${2:-electrs}" + if $COMPOSE ps |grep -q $esplora_service_name; then + for SRV in socat $esplora_sub_service_name; do + $COMPOSE exec $esplora_service_name bash -c "sv -w 60 force-stop /etc/service/$SRV" + done + fi +} + +_stop_services() { + if [ "$PROFILE" == "esplora" ]; then + _stop_esplora esplora_1 + _stop_esplora esplora_2 + _stop_esplora esplora_3 + fi + # bring all services down + $COMPOSE --profile '*' down -v --remove-orphans +} + +_start_services() { + _stop_services + mkdir -p $TEST_DATA_DIR + for port in "${EXPOSED_PORTS[@]}"; do + if [ -n "$(ss -HOlnt "sport = :$port")" ];then + _die "port $port is already bound, services can't be started" + fi + done + $COMPOSE up -d +} + +COMPOSE="docker compose" +if ! $COMPOSE >/dev/null; then + _die "could not call docker compose (hint: install docker compose plugin)" fi -COMPOSE_BASE="$COMPOSE_BASE -f tests/docker-compose.yml" +COMPOSE="$COMPOSE -f tests/docker-compose.yml" PROFILE=${PROFILE:-"esplora"} -COMPOSE="$COMPOSE_BASE --profile $PROFILE" +COMPOSE="$COMPOSE --profile $PROFILE" TEST_DATA_DIR="./test-data" # see docker-compose.yml for the exposed ports if [ "$PROFILE" == "esplora" ]; then - BCLI="$COMPOSE exec -T esplora cli" - EXPOSED_PORTS=(8094 50002) + BCLI_1="$COMPOSE exec -T esplora_1 cli" + BCLI_2="$COMPOSE exec -T esplora_2 cli" + BCLI_3="$COMPOSE exec -T esplora_3 cli" + EXPOSED_PORTS=(8094 8095 8096 50004 50005 50006) elif [ "$PROFILE" == "electrum" ]; then - BCLI="$COMPOSE exec -T -u blits bitcoind bitcoin-cli -regtest" - EXPOSED_PORTS=(50001) + BCLI_1="$COMPOSE exec -T -u blits bitcoind_1 bitcoin-cli -regtest" + BCLI_2="$COMPOSE exec -T -u blits bitcoind_2 bitcoin-cli -regtest" + BCLI_3="$COMPOSE exec -T -u blits bitcoind_3 bitcoin-cli -regtest" + EXPOSED_PORTS=(50001 50002 50003) else _die "invalid profile" fi # restart services (down + up) checking for ports availability -$COMPOSE_BASE --profile '*' down -v --remove-orphans -mkdir -p $TEST_DATA_DIR -for port in "${EXPOSED_PORTS[@]}"; do - if [ -n "$(ss -HOlnt "sport = :$port")" ];then - _die "port $port is already bound, services can't be started" - fi -done -$COMPOSE up -d +_start_services # wait for services (pre-mining) if [ "$PROFILE" == "esplora" ]; then - # wait for esplora to have completed setup - until $COMPOSE logs esplora |grep -q 'Bootstrapped 100%'; do - sleep 1 - done + _wait_for_esplora esplora_1 + _wait_for_esplora esplora_2 + _wait_for_esplora esplora_3 + _stop_esplora esplora_1 tor + _stop_esplora esplora_2 tor + _stop_esplora esplora_3 tor elif [ "$PROFILE" == "electrum" ]; then - # wait for bitcoind to be up - until $COMPOSE logs bitcoind |grep 'Bound to'; do - sleep 1 - done + _wait_for_bitcoind bitcoind_1 + _wait_for_bitcoind bitcoind_2 + _wait_for_bitcoind bitcoind_3 fi -# prepare bitcoin funds -$BCLI createwallet miner -$BCLI -rpcwallet=miner -generate 103 +_prepare_bitcoin_nodes # wait for services (post-mining) -if [ "$PROFILE" == "esplora" ]; then - # wait for esplora to have completed setup - until $COMPOSE logs esplora |grep -q 'Electrum RPC server running'; do - sleep 1 - done -elif [ "$PROFILE" == "electrum" ]; then - # wait for electrs to have completed startup - until $COMPOSE logs electrs |grep 'finished full compaction'; do - sleep 1 - done +if [ "$PROFILE" == "electrum" ]; then + _wait_for_electrs electrs_1 + _wait_for_electrs electrs_2 + _wait_for_electrs electrs_3 fi diff --git a/tests/transfers.rs b/tests/transfers.rs index daff23b..db32857 100644 --- a/tests/transfers.rs +++ b/tests/transfers.rs @@ -1087,7 +1087,7 @@ fn receive_from_unbroadcasted_transfer_to_blinded() { let (consignment, tx) = wlt_2.transfer(invoice, Some(2000), None, true, None); wlt_2.mine_tx(&tx.txid(), false); - // consignemnt validation fails because it notices an unbroadcasted TX in the history + // consignment validation fails because it notices an unbroadcasted TX in the history let res = consignment.validate(&wlt_3.get_resolver(), wlt_3.testnet()); assert!(res.is_err()); let validation_status = match res { @@ -1248,3 +1248,473 @@ fn blank_tapret_opret(#[case] close_method_0: CloseMethod, #[case] close_method_ None, ); } + +#[rstest] +#[case(HistoryType::Linear, ReorgType::ChangeOrder)] +#[ignore = "fix needed"] +#[case(HistoryType::Linear, ReorgType::Revert)] +#[case(HistoryType::Branching, ReorgType::ChangeOrder)] +#[ignore = "fix needed"] +#[case(HistoryType::Branching, ReorgType::Revert)] +#[case(HistoryType::Merging, ReorgType::ChangeOrder)] +#[ignore = "fix needed"] +#[case(HistoryType::Merging, ReorgType::Revert)] +#[serial] +fn reorg_history(#[case] history_type: HistoryType, #[case] reorg_type: ReorgType) { + println!("history_type {history_type:?} reorg_type {reorg_type:?}"); + + initialize(); + connect_reorg_nodes(); + + let mut wlt_1 = get_wallet_custom(&DescriptorType::Wpkh, INSTANCE_2); + let mut wlt_2 = get_wallet_custom(&DescriptorType::Wpkh, INSTANCE_2); + + let (contract_id, iface_type_name) = match history_type { + HistoryType::Linear | HistoryType::Branching => { + wlt_1.issue_nia(600, wlt_1.close_method(), None) + } + HistoryType::Merging => { + let asset_info = AssetInfo::default_nia(vec![400, 200]); + wlt_1.issue_with_info(asset_info, wlt_1.close_method(), vec![None, None]) + } + }; + + let utxo_wlt_1_1 = wlt_1.get_utxo(None); + let utxo_wlt_1_2 = wlt_1.get_utxo(None); + let utxo_wlt_2_1 = wlt_2.get_utxo(None); + let utxo_wlt_2_2 = wlt_2.get_utxo(None); + mine_custom(false, INSTANCE_2, 6); + disconnect_reorg_nodes(); + + let txs = match history_type { + HistoryType::Linear => { + let amt_0 = 590; + let invoice = wlt_2.invoice( + contract_id, + &iface_type_name, + amt_0, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_2_1)), + ); + let (_, tx_0) = wlt_1.send_to_invoice(&mut wlt_2, invoice, Some(1000), None, None); + + let amt_1 = 100; + let invoice = wlt_1.invoice( + contract_id, + &iface_type_name, + amt_1, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_1_1)), + ); + let (_, tx_1) = wlt_2.send_to_invoice(&mut wlt_1, invoice, Some(1000), None, None); + + let amt_2 = 80; + let invoice = wlt_2.invoice( + contract_id, + &iface_type_name, + amt_2, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_2_2)), + ); + let (_, tx_2) = wlt_1.send_to_invoice(&mut wlt_2, invoice, Some(1000), None, None); + + vec![tx_0, tx_1, tx_2] + } + HistoryType::Branching => { + let amt_0 = 600; + let invoice = wlt_2.invoice( + contract_id, + &iface_type_name, + amt_0, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_2_1)), + ); + let (_, tx_0) = wlt_1.send_to_invoice(&mut wlt_2, invoice, Some(1000), None, None); + + let amt_1 = 200; + let invoice = wlt_1.invoice( + contract_id, + &iface_type_name, + amt_1, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_1_1)), + ); + let (_, tx_1) = wlt_2.send_to_invoice(&mut wlt_1, invoice, Some(1000), None, None); + + let amt_2 = amt_0 - amt_1 - 1; + let invoice = wlt_1.invoice( + contract_id, + &iface_type_name, + amt_2, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_1_2)), + ); + let (_, tx_2) = wlt_2.send_to_invoice(&mut wlt_1, invoice, Some(1000), None, None); + + vec![tx_0, tx_1, tx_2] + } + HistoryType::Merging => { + let amt_0 = 400; + let invoice = wlt_2.invoice( + contract_id, + &iface_type_name, + amt_0, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_2_1)), + ); + let (_, tx_0) = wlt_1.send_to_invoice(&mut wlt_2, invoice, None, None, None); + + let amt_1 = 200; + let invoice = wlt_2.invoice( + contract_id, + &iface_type_name, + amt_1, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_2_2)), + ); + let (_, tx_1) = wlt_1.send_to_invoice(&mut wlt_2, invoice, None, None, None); + + let amt_2 = amt_0 + amt_1 - 1; + let invoice = wlt_1.invoice( + contract_id, + &iface_type_name, + amt_2, + CloseMethod::OpretFirst, + InvoiceType::Blinded(Some(utxo_wlt_1_1)), + ); + let (_, tx_2) = wlt_2.send_to_invoice(&mut wlt_1, invoice, None, None, None); + + vec![tx_0, tx_1, tx_2] + } + }; + + match (history_type, reorg_type) { + (HistoryType::Linear, ReorgType::ChangeOrder) => { + broadcast_tx_and_mine(&txs[2], INSTANCE_3); + broadcast_tx_and_mine(&txs[1], INSTANCE_3); + broadcast_tx_and_mine(&txs[0], INSTANCE_3); + wlt_1.switch_to_instance(INSTANCE_3); + wlt_2.switch_to_instance(INSTANCE_3); + let wlt_1_alloc_1 = 10; + let wlt_1_alloc_2 = 20; + let wlt_2_alloc_1 = 490; + let wlt_2_alloc_2 = 80; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1, wlt_1_alloc_2], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1, wlt_2_alloc_2], + false, + ); + } + (HistoryType::Linear | HistoryType::Branching, ReorgType::Revert) => { + broadcast_tx_and_mine(&txs[1], INSTANCE_3); + broadcast_tx_and_mine(&txs[2], INSTANCE_3); + wlt_1.switch_to_instance(INSTANCE_3); + wlt_2.switch_to_instance(INSTANCE_3); + let wlt_1_alloc_1 = 600; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![], + false, + ); + } + (HistoryType::Branching, ReorgType::ChangeOrder) => { + broadcast_tx_and_mine(&txs[1], INSTANCE_3); + broadcast_tx_and_mine(&txs[2], INSTANCE_3); + broadcast_tx_and_mine(&txs[0], INSTANCE_3); + wlt_1.switch_to_instance(INSTANCE_3); + wlt_2.switch_to_instance(INSTANCE_3); + let wlt_1_alloc_1 = 200; + let wlt_1_alloc_2 = 399; + let wlt_2_alloc_1 = 1; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1, wlt_1_alloc_2], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1], + false, + ); + } + (HistoryType::Merging, ReorgType::ChangeOrder) => { + broadcast_tx_and_mine(&txs[1], INSTANCE_3); + broadcast_tx_and_mine(&txs[0], INSTANCE_3); + broadcast_tx_and_mine(&txs[2], INSTANCE_3); + wlt_1.switch_to_instance(INSTANCE_3); + wlt_2.switch_to_instance(INSTANCE_3); + let wlt_1_alloc_1 = 599; + let wlt_2_alloc_1 = 1; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1], + false, + ); + } + (HistoryType::Merging, ReorgType::Revert) => { + broadcast_tx_and_mine(&txs[1], INSTANCE_3); + broadcast_tx_and_mine(&txs[2], INSTANCE_3); + wlt_1.switch_to_instance(INSTANCE_3); + wlt_2.switch_to_instance(INSTANCE_3); + let wlt_1_alloc_1 = 400; + let wlt_2_alloc_1 = 200; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1], + false, + ); + } + } + + mine_custom(false, INSTANCE_3, 3); + connect_reorg_nodes(); + wlt_1.switch_to_instance(INSTANCE_2); + wlt_2.switch_to_instance(INSTANCE_2); + + let mut wlt_3 = get_wallet_custom(&DescriptorType::Wpkh, INSTANCE_2); + + match history_type { + HistoryType::Linear => { + let wlt_1_alloc_1 = 10; + let wlt_1_alloc_2 = 20; + let wlt_1_amt = wlt_1_alloc_1 + wlt_1_alloc_2; + let wlt_2_alloc_1 = 490; + let wlt_2_alloc_2 = 80; + let wlt_2_amt = wlt_2_alloc_1 + wlt_2_alloc_2; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1, wlt_1_alloc_2], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1, wlt_2_alloc_2], + false, + ); + wlt_1.send( + &mut wlt_3, + TransferType::Witness, + contract_id, + &iface_type_name, + wlt_1_amt, + 1000, + None, + ); + wlt_2.send( + &mut wlt_3, + TransferType::Witness, + contract_id, + &iface_type_name, + wlt_2_amt, + 1000, + None, + ); + wlt_3.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_amt, wlt_2_amt], + false, + ); + } + HistoryType::Branching => { + let wlt_1_alloc_1 = 200; + let wlt_1_alloc_2 = 399; + let wlt_1_amt = wlt_1_alloc_1 + wlt_1_alloc_2; + let wlt_2_alloc_1 = 1; + let wlt_2_amt = wlt_2_alloc_1; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1, wlt_1_alloc_2], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1], + false, + ); + wlt_1.send( + &mut wlt_3, + TransferType::Witness, + contract_id, + &iface_type_name, + wlt_1_amt, + 1000, + None, + ); + wlt_2.send( + &mut wlt_3, + TransferType::Witness, + contract_id, + &iface_type_name, + wlt_2_amt, + 1000, + None, + ); + wlt_3.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_amt, wlt_2_amt], + false, + ); + } + HistoryType::Merging => { + let wlt_1_alloc_1 = 599; + let wlt_1_amt = wlt_1_alloc_1; + let wlt_2_alloc_1 = 1; + let wlt_2_amt = wlt_2_alloc_1; + wlt_1.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_alloc_1], + false, + ); + wlt_2.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_2_alloc_1], + false, + ); + wlt_1.send( + &mut wlt_3, + TransferType::Witness, + contract_id, + &iface_type_name, + wlt_1_amt, + 1000, + None, + ); + wlt_2.send( + &mut wlt_3, + TransferType::Witness, + contract_id, + &iface_type_name, + wlt_2_amt, + 1000, + None, + ); + wlt_3.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![wlt_1_amt, wlt_2_amt], + false, + ); + } + } +} + +#[rstest] +#[ignore = "fix needed"] +#[case(false)] +#[case(true)] +#[serial] +fn revert_genesis(#[case] with_transfers: bool) { + println!("with_transfers {with_transfers}"); + + initialize(); + // connecting before disconnecting since disconnect is not idempotent + connect_reorg_nodes(); + disconnect_reorg_nodes(); + + let mut wlt = get_wallet_custom(&DescriptorType::Wpkh, INSTANCE_2); + + let issued_supply = 600; + let utxo = wlt.get_utxo(None); + let (contract_id, iface_type_name) = + wlt.issue_nia(issued_supply, wlt.close_method(), Some(&utxo)); + wlt.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![issued_supply], + false, + ); + + if with_transfers { + let mut recv_wlt = get_wallet_custom(&DescriptorType::Wpkh, INSTANCE_2); + let amt = 200; + wlt.send( + &mut recv_wlt, + TransferType::Blinded, + contract_id, + &iface_type_name, + amt, + 1000, + None, + ); + wlt.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![issued_supply - amt], + false, + ); + } + + assert!(matches!( + wlt.get_witness_ord(&utxo.txid), + WitnessOrd::Mined(_) + )); + wlt.switch_to_instance(INSTANCE_3); + assert_eq!(wlt.get_witness_ord(&utxo.txid), WitnessOrd::Archived); + + wlt.check_allocations( + contract_id, + &iface_type_name, + AssetSchema::Nia, + vec![], + false, + ); +} diff --git a/tests/utils/chain.rs b/tests/utils/chain.rs index d824d4b..5b0b276 100644 --- a/tests/utils/chain.rs +++ b/tests/utils/chain.rs @@ -41,7 +41,7 @@ pub fn initialize() { println!("{output:?}"); panic!("failed to start test services"); } - _wait_indexer_sync(); + (INSTANCE_1..=INSTANCE_3).for_each(_wait_indexer_sync); }); } @@ -52,50 +52,62 @@ pub struct Miner { no_mine_count: u32, } -fn _bitcoin_cli() -> Vec { +fn _service_base_name() -> String { + match INDEXER.get().unwrap() { + Indexer::Electrum => "bitcoind", + Indexer::Esplora => "esplora", + } + .to_string() +} + +fn _bitcoin_cli_cmd(instance: u8, args: Vec<&str>) -> String { let compose_file = PathBuf::from("tests").join("docker-compose.yml"); - let mut cmd = vec![ + let mut bitcoin_cli = vec![ s!("-f"), compose_file.to_string_lossy().to_string(), s!("exec"), s!("-T"), ]; + let service_name = format!("{}_{instance}", _service_base_name()); match INDEXER.get().unwrap() { - Indexer::Electrum => cmd.extend(vec![ + Indexer::Electrum => bitcoin_cli.extend(vec![ "-u".to_string(), "blits".to_string(), - "bitcoind".to_string(), + service_name, "bitcoin-cli".to_string(), "-regtest".to_string(), ]), - Indexer::Esplora => cmd.extend(vec!["esplora".to_string(), "cli".to_string()]), + Indexer::Esplora => bitcoin_cli.extend(vec![service_name, "cli".to_string()]), }; - cmd + let output = Command::new("docker") + .stdin(Stdio::null()) + .stderr(Stdio::null()) + .arg("compose") + .args(bitcoin_cli) + .args(&args) + .output() + .unwrap_or_else(|_| panic!("failed to call bitcoind with args {args:?}")); + if !output.status.success() { + println!("{output:?}"); + panic!("failed to get succesful output with args {args:?}"); + } + String::from_utf8(output.stdout).unwrap().trim().to_string() } impl Miner { - fn mine(&self) -> bool { + fn mine(&self, instance: u8, blocks: u32) -> bool { if self.no_mine_count > 0 { return false; } - self.force_mine() + self.force_mine(instance, blocks) } - fn force_mine(&self) -> bool { - let output = Command::new("docker") - .stdin(Stdio::null()) - .arg("compose") - .args(_bitcoin_cli()) - .arg("-rpcwallet=miner") - .arg("-generate") - .arg("1") - .output() - .expect("failed to mine"); - if !output.status.success() { - println!("{output:?}"); - panic!("failed to mine"); - } - _wait_indexer_sync(); + fn force_mine(&self, instance: u8, blocks: u32) -> bool { + _bitcoin_cli_cmd( + instance, + vec!["-rpcwallet=miner", "-generate", &blocks.to_string()], + ); + _wait_indexer_sync(instance); true } @@ -111,6 +123,10 @@ impl Miner { } pub fn mine(resume: bool) { + mine_custom(resume, INSTANCE_1, 1); +} + +pub fn mine_custom(resume: bool, instance: u8, blocks: u32) { let t_0 = OffsetDateTime::now_utc(); if resume { resume_mining(); @@ -120,7 +136,7 @@ pub fn mine(resume: bool) { println!("forcibly breaking mining wait"); resume_mining(); } - let mined = MINER.read().as_ref().unwrap().mine(); + let mined = MINER.read().as_ref().unwrap().mine(instance, blocks); if mined { break; } @@ -129,6 +145,10 @@ pub fn mine(resume: bool) { } pub fn mine_but_no_resume() { + mine_but_no_resume_custom(INSTANCE_1, 1); +} + +pub fn mine_but_no_resume_custom(instance: u8, blocks: u32) { let t_0 = OffsetDateTime::now_utc(); loop { if (OffsetDateTime::now_utc() - t_0).as_seconds_f32() > 120.0 { @@ -137,7 +157,7 @@ pub fn mine_but_no_resume() { } let miner = MINER.write().unwrap(); if miner.no_mine_count <= 1 { - miner.force_mine(); + miner.force_mine(instance, blocks); break; } drop(miner); @@ -170,42 +190,78 @@ pub fn resume_mining() { MINER.write().unwrap().resume_mining() } -pub fn get_height() -> u32 { - let output = Command::new("docker") - .stdin(Stdio::null()) - .stderr(Stdio::null()) - .arg("compose") - .args(_bitcoin_cli()) - .arg("getblockcount") - .output() - .expect("failed to call getblockcount"); - if !output.status.success() { - println!("{output:?}"); - panic!("failed to get block count"); +fn _get_connection_tuple() -> Vec<(u8, String)> { + let serive_base_name = _service_base_name(); + vec![ + (INSTANCE_3, format!("{serive_base_name}_{INSTANCE_2}:18444")), + (INSTANCE_2, format!("{serive_base_name}_{INSTANCE_3}:18444")), + ] +} + +pub fn connect_reorg_nodes() { + for (instance, node_addr) in _get_connection_tuple() { + _bitcoin_cli_cmd(instance, vec!["addnode", &node_addr, "onetry"]); + } + let t_0 = OffsetDateTime::now_utc(); + loop { + if (OffsetDateTime::now_utc() - t_0).as_seconds_f32() > 20.0 { + panic!("nodes are not syncing with each other") + } + let height_2 = get_height_custom(INSTANCE_2); + let height_3 = get_height_custom(INSTANCE_3); + if height_2 == height_3 { + break; + } + std::thread::sleep(Duration::from_millis(500)); } - let blockcount_str = - std::str::from_utf8(&output.stdout).expect("could not parse blockcount output"); - blockcount_str - .trim() +} + +pub fn disconnect_reorg_nodes() { + for (instance, node_addr) in _get_connection_tuple() { + _bitcoin_cli_cmd(instance, vec!["disconnectnode", &node_addr]); + } +} + +pub fn get_height() -> u32 { + get_height_custom(INSTANCE_1) +} + +pub fn get_height_custom(instance: u8) -> u32 { + _bitcoin_cli_cmd(instance, vec!["getblockcount"]) .parse::() .expect("could not parse blockcount") } -fn _wait_indexer_sync() { +pub fn indexer_url(instance: u8, network: Network) -> String { + match (INDEXER.get().unwrap(), network, instance) { + (Indexer::Electrum, Network::Mainnet, _) => ELECTRUM_MAINNET_URL, + (Indexer::Electrum, Network::Regtest, INSTANCE_1) => ELECTRUM_1_REGTEST_URL, + (Indexer::Electrum, Network::Regtest, INSTANCE_2) => ELECTRUM_2_REGTEST_URL, + (Indexer::Electrum, Network::Regtest, INSTANCE_3) => ELECTRUM_3_REGTEST_URL, + (Indexer::Esplora, Network::Mainnet, _) => ESPLORA_MAINNET_URL, + (Indexer::Esplora, Network::Regtest, INSTANCE_1) => ESPLORA_1_REGTEST_URL, + (Indexer::Esplora, Network::Regtest, INSTANCE_2) => ESPLORA_2_REGTEST_URL, + (Indexer::Esplora, Network::Regtest, INSTANCE_3) => ESPLORA_3_REGTEST_URL, + _ => unreachable!(), + } + .to_string() +} + +fn _wait_indexer_sync(instance: u8) { let t_0 = OffsetDateTime::now_utc(); - let blockcount = get_height(); + let blockcount = get_height_custom(instance); loop { std::thread::sleep(Duration::from_millis(100)); + let url = &indexer_url(instance, Network::Regtest); match INDEXER.get().unwrap() { Indexer::Electrum => { - let electrum_client = - ElectrumClient::new(ELECTRUM_REGTEST_URL).expect("cannot get electrum client"); + let electrum_client = ElectrumClient::new(url).unwrap(); if electrum_client.block_header(blockcount as usize).is_ok() { break; } } Indexer::Esplora => { - let esplora_client = EsploraClient::new_esplora(ESPLORA_REGTEST_URL).unwrap(); + let esplora_client = EsploraClient::new_esplora(url).unwrap(); if esplora_client.block_hash(blockcount).is_ok() { break; } @@ -217,28 +273,17 @@ fn _wait_indexer_sync() { } } -pub fn send_to_address(address: String, sats: Option) -> String { +fn _send_to_address(address: &str, sats: Option, instance: u8) -> String { let sats = Sats::from_sats(sats.unwrap_or(100_000_000)); let btc = format!("{}.{:0>8}", sats.btc_floor(), sats.sats_rem()); - let output = Command::new("docker") - .stdin(Stdio::null()) - .arg("compose") - .args(_bitcoin_cli()) - .arg("-rpcwallet=miner") - .arg("sendtoaddress") - .arg(address) - .arg(btc) - .output() - .expect("failed to fund wallet"); - if !output.status.success() { - println!("{output:?}"); - panic!("failed to send to address"); - } - String::from_utf8(output.stdout).unwrap().trim().to_string() + _bitcoin_cli_cmd( + instance, + vec!["-rpcwallet=miner", "sendtoaddress", address, &btc], + ) } -pub fn fund_wallet(address: String, sats: Option) -> String { - let txid = send_to_address(address, sats); - mine(false); +pub fn fund_wallet(address: String, sats: Option, instance: u8) -> String { + let txid = _send_to_address(&address, sats, instance); + mine_custom(false, instance, 1); txid } diff --git a/tests/utils/helpers.rs b/tests/utils/helpers.rs index 734818f..2d21685 100644 --- a/tests/utils/helpers.rs +++ b/tests/utils/helpers.rs @@ -5,6 +5,7 @@ pub struct TestWallet { descriptor: RgbDescr, signer: Option, wallet_dir: PathBuf, + instance: u8, } enum WalletAccount { @@ -76,6 +77,19 @@ impl fmt::Display for DescriptorType { } } +#[derive(Debug, Copy, Clone)] +pub enum HistoryType { + Linear, + Branching, + Merging, +} + +#[derive(Debug, Copy, Clone)] +pub enum ReorgType { + ChangeOrder, + Revert, +} + #[derive(Debug, Copy, Clone)] pub enum TransferType { Blinded, @@ -494,6 +508,7 @@ fn _get_wallet( network: Network, wallet_dir: PathBuf, wallet_account: WalletAccount, + instance: u8, ) -> TestWallet { std::fs::create_dir_all(&wallet_dir).unwrap(); println!("wallet dir: {wallet_dir:?}"); @@ -551,6 +566,7 @@ fn _get_wallet( descriptor, signer, wallet_dir, + instance, }; // TODO: remove if once found solution for esplora 'Too many requests' error @@ -562,6 +578,10 @@ fn _get_wallet( } pub fn get_wallet(descriptor_type: &DescriptorType) -> TestWallet { + get_wallet_custom(descriptor_type, INSTANCE_1) +} + +pub fn get_wallet_custom(descriptor_type: &DescriptorType, instance: u8) -> TestWallet { let mut seed = vec![0u8; 128]; rand::thread_rng().fill_bytes(&mut seed); @@ -577,6 +597,7 @@ pub fn get_wallet(descriptor_type: &DescriptorType) -> TestWallet { Network::Regtest, wallet_dir, WalletAccount::Private(xpriv_account), + instance, ) } @@ -594,9 +615,45 @@ pub fn get_mainnet_wallet() -> TestWallet { Network::Mainnet, wallet_dir, WalletAccount::Public(xpub_account), + INSTANCE_1, ) } +fn get_indexer(indexer_url: &str) -> AnyIndexer { + match INDEXER.get().unwrap() { + Indexer::Electrum => { + AnyIndexer::Electrum(Box::new(ElectrumClient::new(indexer_url).unwrap())) + } + Indexer::Esplora => { + AnyIndexer::Esplora(Box::new(EsploraClient::new_esplora(indexer_url).unwrap())) + } + } +} + +fn get_resolver(indexer_url: &str) -> AnyResolver { + match INDEXER.get().unwrap() { + Indexer::Electrum => AnyResolver::electrum_blocking(indexer_url, None).unwrap(), + Indexer::Esplora => AnyResolver::esplora_blocking(indexer_url, None).unwrap(), + } +} + +fn broadcast_tx(tx: &Tx, indexer_url: &str) { + match get_indexer(indexer_url) { + AnyIndexer::Electrum(inner) => { + inner.transaction_broadcast(tx).unwrap(); + } + AnyIndexer::Esplora(inner) => { + inner.publish(tx).unwrap(); + } + _ => unreachable!("unsupported indexer"), + } +} + +pub fn broadcast_tx_and_mine(tx: &Tx, instance: u8) { + broadcast_tx(tx, &indexer_url(instance, Network::Regtest)); + mine_custom(false, instance, 1); +} + pub fn attachment_from_fpath(fpath: &str) -> Attachment { let file_bytes = std::fs::read(fpath).unwrap(); let file_hash: sha256::Hash = Hash::hash(&file_bytes[..]); @@ -667,7 +724,7 @@ impl TestWallet { pub fn get_utxo(&mut self, sats: Option) -> Outpoint { let address = self.get_address(); - let txid = Txid::from_str(&fund_wallet(address.to_string(), sats)).unwrap(); + let txid = Txid::from_str(&fund_wallet(address.to_string(), sats, self.instance)).unwrap(); self.sync(); let mut vout = None; let coins = self.wallet.wallet().address_coins(); @@ -685,60 +742,40 @@ impl TestWallet { } } - fn get_indexer_url(&self) -> String { - match INDEXER.get().unwrap() { - Indexer::Electrum => match self.network() { - Network::Regtest => ELECTRUM_REGTEST_URL, - Network::Mainnet => ELECTRUM_MAINNET_URL, - _ => unimplemented!("network not yet supported"), - }, - Indexer::Esplora => match self.network() { - Network::Regtest => ESPLORA_REGTEST_URL, - Network::Mainnet => ESPLORA_MAINNET_URL, - _ => unimplemented!("network not yet supported"), - }, - } - .to_string() + pub fn change_instance(&mut self, instance: u8) { + self.instance = instance; + } + + pub fn switch_to_instance(&mut self, instance: u8) { + self.change_instance(instance); + self.sync(); + self.update_witnesses(1); + } + + pub fn indexer_url(&self) -> String { + indexer_url(self.instance, self.network()) } fn get_indexer(&self) -> AnyIndexer { - let indexer_url = self.get_indexer_url(); - match INDEXER.get().unwrap() { - Indexer::Electrum => { - AnyIndexer::Electrum(Box::new(ElectrumClient::new(&indexer_url).unwrap())) - } - Indexer::Esplora => { - AnyIndexer::Esplora(Box::new(EsploraClient::new_esplora(&indexer_url).unwrap())) - } - } + get_indexer(&self.indexer_url()) } pub fn get_resolver(&self) -> AnyResolver { - let indexer_url = self.get_indexer_url(); - match INDEXER.get().unwrap() { - Indexer::Electrum => AnyResolver::electrum_blocking(&indexer_url, None).unwrap(), - Indexer::Esplora => AnyResolver::esplora_blocking(&indexer_url, None).unwrap(), - } + get_resolver(&self.indexer_url()) } pub fn broadcast_tx(&self, tx: &Tx) { - match self.get_indexer() { - AnyIndexer::Electrum(inner) => { - inner.transaction_broadcast(tx).unwrap(); - } - AnyIndexer::Esplora(inner) => { - inner.publish(tx).unwrap(); - } - _ => unreachable!("unsupported indexer"), - } + broadcast_tx(tx, &self.indexer_url()); } - pub fn get_tx_height(&self, txid: &Txid) -> Option { - match self - .get_resolver() + pub fn get_witness_ord(&self, txid: &Txid) -> WitnessOrd { + self.get_resolver() .resolve_pub_witness_ord(XWitnessId::Bitcoin(*txid)) .unwrap() - { + } + + pub fn get_tx_height(&self, txid: &Txid) -> Option { + match self.get_witness_ord(txid) { WitnessOrd::Mined(witness_pos) => Some(witness_pos.height().get()), _ => None, } @@ -760,7 +797,7 @@ impl TestWallet { pub fn mine_tx(&self, txid: &Txid, resume: bool) { let mut attempts = 10; loop { - mine(resume); + mine_custom(resume, self.instance, 1); if self.get_tx_height(txid).is_some() { break; } @@ -1049,9 +1086,13 @@ impl TestWallet { .unwrap() } + pub fn list_contracts(&self) -> Vec { + self.wallet.stock().contracts().unwrap().collect() + } + pub fn debug_contracts(&self) { println!("Contracts:"); - for info in self.wallet.stock().contracts().unwrap() { + for info in self.list_contracts() { println!("{}", info.to_string().replace("\n", "\t")); } } @@ -1261,7 +1302,10 @@ impl TestWallet { AssetSchema::Nia | AssetSchema::Cfa => { let allocations = self.contract_fungible_allocations(contract_id, iface_type_name, false); - assert_eq!(allocations.len(), expected_fungible_allocations.len()); + if allocations.len() != expected_fungible_allocations.len() { + println!("allocations: {allocations:?}"); + assert_eq!(allocations.len(), expected_fungible_allocations.len()); + } assert!(allocations .iter() .all(|a| a.seal.method() == self.close_method())); diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 7d3837a..256c139 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -5,14 +5,22 @@ pub const TEST_DATA_DIR: &str = "test-data"; pub const INTEGRATION_DATA_DIR: &str = "integration"; pub const STRESS_DATA_DIR: &str = "stress"; -pub const ELECTRUM_REGTEST_URL: &str = "127.0.0.1:50001"; +pub const ELECTRUM_1_REGTEST_URL: &str = "127.0.0.1:50001"; +pub const ELECTRUM_2_REGTEST_URL: &str = "127.0.0.1:50002"; +pub const ELECTRUM_3_REGTEST_URL: &str = "127.0.0.1:50003"; pub const ELECTRUM_MAINNET_URL: &str = "ssl://electrum.iriswallet.com:50003"; -pub const ESPLORA_REGTEST_URL: &str = "http://127.0.0.1:8094/regtest/api"; +pub const ESPLORA_1_REGTEST_URL: &str = "http://127.0.0.1:8094/regtest/api"; +pub const ESPLORA_2_REGTEST_URL: &str = "http://127.0.0.1:8095/regtest/api"; +pub const ESPLORA_3_REGTEST_URL: &str = "http://127.0.0.1:8096/regtest/api"; pub const ESPLORA_MAINNET_URL: &str = "https://blockstream.info/api"; pub const FAKE_TXID: &str = "e5a3e577309df31bd606f48049049d2e1e02b048206ba232944fcc053a176ccb:0"; pub const UDA_FIXED_INDEX: u32 = 0; pub const DEFAULT_FEE_ABS: u64 = 400; +pub const INSTANCE_1: u8 = 1; +pub const INSTANCE_2: u8 = 2; +pub const INSTANCE_3: u8 = 3; + pub use std::{ cell::OnceCell, collections::{BTreeMap, BTreeSet, HashMap, HashSet}, @@ -63,6 +71,7 @@ pub use psbt::{ pub use psrgbt::{RgbExt, RgbInExt, RgbPsbt, TxParams}; pub use rand::RngCore; pub use rgb::{ + info::ContractInfo, interface::{AllocatedState, AssignmentsFilter, ContractOp, OpDirection}, invoice::Pay2Vout, persistence::{MemContract, MemContractState, Stock}, @@ -93,6 +102,7 @@ pub use rgbstd::{ }; pub use rstest::rstest; pub use schemata::{CollectibleFungibleAsset, NonInflatableAsset, UniqueDigitalAsset}; +pub use serial_test::serial; pub use strict_encoding::{fname, tn, FieldName, StrictSerialize, TypeName}; pub use strict_types::{StrictVal, TypeSystem}; pub use strum::IntoEnumIterator;