From 8d8e505395e9cf6baa0919ee202ea043fd5910c0 Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 29 Nov 2024 17:25:10 +0900 Subject: [PATCH 1/2] feat: generic transfers --- .github/workflows/benchmark-prs.yml | 34 - .github/workflows/merge.yml | 76 +- .github/workflows/nightly.yml | 4 - .github/workflows/nightly_wan.yml | 14 +- Cargo.lock | 202 +-- Cargo.toml | 1 - README.md | 2 - autonomi/examples/metamask/index.js | 6 +- autonomi/src/client/wasm.rs | 14 +- autonomi/tests/external_signer.rs | 19 +- evmlib/artifacts/AutonomiNetworkToken.json | 12 +- evmlib/src/contract/network_token.rs | 30 +- evmlib/src/external_signer.rs | 16 +- evmlib/src/wallet.rs | 40 +- evmlib/tests/network_token.rs | 14 +- node-launchpad/.config/config.json5 | 6 +- node-launchpad/src/action.rs | 2 +- node-launchpad/src/app.rs | 12 +- node-launchpad/src/tui.rs | 2 +- sn_auditor/CHANGELOG.md | 137 -- sn_auditor/Cargo.toml | 51 - sn_auditor/README.md | 60 - sn_auditor/resources/dag.svg | 125 -- sn_auditor/src/dag_db.rs | 796 ---------- sn_auditor/src/main.rs | 416 ------ sn_auditor/src/routes.rs | 142 -- sn_evm/src/lib.rs | 2 +- sn_logging/src/layers.rs | 1 - sn_networking/Cargo.toml | 1 - sn_networking/src/cmd.rs | 2 +- sn_networking/src/error.rs | 16 +- sn_networking/src/event/kad.rs | 29 +- sn_networking/src/event/request_response.rs | 2 +- sn_networking/src/lib.rs | 37 +- sn_networking/src/record_store.rs | 6 +- sn_networking/src/replication_fetcher.rs | 2 +- sn_networking/src/spends.rs | 69 - sn_networking/src/transactions.rs | 50 + sn_networking/src/transfers.rs | 223 --- sn_node/Cargo.toml | 4 - sn_node/README.md | 2 +- sn_node/src/bin/safenode/main.rs | 1 - sn_node/src/error.rs | 4 - sn_node/src/log_markers.rs | 4 +- sn_node/src/metrics.rs | 2 +- sn_node/src/put_validation.rs | 349 +---- sn_node/tests/double_spend.rs | 683 --------- sn_node/tests/sequential_transfers.rs | 54 - sn_node/tests/spend_simulation.rs | 1162 --------------- sn_node_manager/Cargo.toml | 1 - sn_node_manager/src/cmd/faucet.rs | 2 +- sn_node_manager/src/cmd/node.rs | 10 +- sn_node_manager/src/helpers.rs | 11 + sn_node_manager/src/lib.rs | 15 +- sn_node_manager/src/local.rs | 4 +- sn_node_rpc_client/Cargo.toml | 1 - sn_node_rpc_client/src/main.rs | 1 - sn_protocol/Cargo.toml | 1 - sn_protocol/README.md | 6 +- sn_protocol/src/lib.rs | 50 +- sn_protocol/src/messages/cmd.rs | 2 +- sn_protocol/src/storage.rs | 4 +- sn_protocol/src/storage/address.rs | 3 +- .../src/storage/address/transaction.rs | 39 + sn_protocol/src/storage/header.rs | 12 +- sn_protocol/src/storage/transaction.rs | 79 + sn_transfers/CHANGELOG.md | 917 ------------ sn_transfers/Cargo.toml | 59 - sn_transfers/README.md | 317 ---- sn_transfers/benches/reissue.rs | 161 --- sn_transfers/dag.svg | 125 -- sn_transfers/src/cashnotes.rs | 163 --- sn_transfers/src/cashnotes/address.rs | 106 -- sn_transfers/src/cashnotes/cashnote.rs | 190 --- sn_transfers/src/cashnotes/hash.rs | 123 -- sn_transfers/src/cashnotes/nano.rs | 210 --- sn_transfers/src/cashnotes/signed_spend.rs | 293 ---- sn_transfers/src/cashnotes/spend_reason.rs | 201 --- sn_transfers/src/cashnotes/unique_keys.rs | 397 ----- sn_transfers/src/error.rs | 77 - sn_transfers/src/genesis.rs | 275 ---- sn_transfers/src/lib.rs | 198 --- sn_transfers/src/transfers.rs | 15 - .../src/transfers/signed_transaction.rs | 183 --- sn_transfers/src/transfers/transfer.rs | 231 --- .../src/transfers/unsigned_transaction.rs | 1128 --------------- sn_transfers/src/wallet.rs | 138 -- sn_transfers/src/wallet/api.rs | 168 --- sn_transfers/src/wallet/authentication.rs | 108 -- sn_transfers/src/wallet/data_payments.rs | 379 ----- sn_transfers/src/wallet/encryption.rs | 291 ---- sn_transfers/src/wallet/error.rs | 135 -- sn_transfers/src/wallet/hot_wallet.rs | 1280 ----------------- sn_transfers/src/wallet/keys.rs | 150 -- sn_transfers/src/wallet/wallet_file.rs | 245 ---- sn_transfers/src/wallet/watch_only.rs | 423 ------ 96 files changed, 468 insertions(+), 13397 deletions(-) delete mode 100644 sn_auditor/CHANGELOG.md delete mode 100644 sn_auditor/Cargo.toml delete mode 100644 sn_auditor/README.md delete mode 100644 sn_auditor/resources/dag.svg delete mode 100644 sn_auditor/src/dag_db.rs delete mode 100644 sn_auditor/src/main.rs delete mode 100644 sn_auditor/src/routes.rs delete mode 100644 sn_networking/src/spends.rs create mode 100644 sn_networking/src/transactions.rs delete mode 100644 sn_networking/src/transfers.rs delete mode 100644 sn_node/tests/double_spend.rs delete mode 100644 sn_node/tests/sequential_transfers.rs delete mode 100644 sn_node/tests/spend_simulation.rs create mode 100644 sn_protocol/src/storage/address/transaction.rs create mode 100644 sn_protocol/src/storage/transaction.rs delete mode 100644 sn_transfers/CHANGELOG.md delete mode 100644 sn_transfers/Cargo.toml delete mode 100644 sn_transfers/README.md delete mode 100644 sn_transfers/benches/reissue.rs delete mode 100644 sn_transfers/dag.svg delete mode 100644 sn_transfers/src/cashnotes.rs delete mode 100644 sn_transfers/src/cashnotes/address.rs delete mode 100644 sn_transfers/src/cashnotes/cashnote.rs delete mode 100644 sn_transfers/src/cashnotes/hash.rs delete mode 100644 sn_transfers/src/cashnotes/nano.rs delete mode 100644 sn_transfers/src/cashnotes/signed_spend.rs delete mode 100644 sn_transfers/src/cashnotes/spend_reason.rs delete mode 100644 sn_transfers/src/cashnotes/unique_keys.rs delete mode 100644 sn_transfers/src/error.rs delete mode 100644 sn_transfers/src/genesis.rs delete mode 100644 sn_transfers/src/lib.rs delete mode 100644 sn_transfers/src/transfers.rs delete mode 100644 sn_transfers/src/transfers/signed_transaction.rs delete mode 100644 sn_transfers/src/transfers/transfer.rs delete mode 100644 sn_transfers/src/transfers/unsigned_transaction.rs delete mode 100644 sn_transfers/src/wallet.rs delete mode 100644 sn_transfers/src/wallet/api.rs delete mode 100644 sn_transfers/src/wallet/authentication.rs delete mode 100644 sn_transfers/src/wallet/data_payments.rs delete mode 100644 sn_transfers/src/wallet/encryption.rs delete mode 100644 sn_transfers/src/wallet/error.rs delete mode 100644 sn_transfers/src/wallet/hot_wallet.rs delete mode 100644 sn_transfers/src/wallet/keys.rs delete mode 100644 sn_transfers/src/wallet/wallet_file.rs delete mode 100644 sn_transfers/src/wallet/watch_only.rs diff --git a/.github/workflows/benchmark-prs.yml b/.github/workflows/benchmark-prs.yml index 13da75ef2d..5978348f45 100644 --- a/.github/workflows/benchmark-prs.yml +++ b/.github/workflows/benchmark-prs.yml @@ -378,37 +378,3 @@ jobs: # Enable Job Summary for PRs summary-always: true - benchmark-cash: - name: Compare sn_transfer benchmarks to main - # right now only ubuntu, running on multiple systems would require many pushes...\ - # perhaps this can be done with one consolidation action in the future, pulling down all results and pushing - # once to the branch.. - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - uses: Swatinem/rust-cache@v2 - continue-on-error: true - - ######################## - ### Setup ### - ######################## - - run: cargo install cargo-criterion - - - name: install ripgrep - run: sudo apt-get -y install ripgrep - - ######################## - ### Benchmark ### - ######################## - - name: Bench `sn_transfers` - shell: bash - # Criterion outputs the actual bench results to stderr "2>&1 tee output.txt" takes stderr, - # passes to tee which displays it in the terminal and writes to output.txt - run: | - cargo criterion --message-format=json 2>&1 -p sn_transfers | tee -a output.txt - cat output.txt diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 23c3e2bb31..67427337e9 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -156,10 +156,6 @@ jobs: timeout-minutes: 25 run: cargo test --release --package sn_protocol - - name: Run transfers tests - timeout-minutes: 25 - run: cargo test --release --package sn_transfers - - name: Run logging tests timeout-minutes: 25 run: cargo test --release --package sn_logging @@ -584,9 +580,9 @@ jobs: log_file_prefix: safe_test_logs_e2e platform: ${{ matrix.os }} - # spend_test: + # transaction_test: # if: "!startsWith(github.event.head_commit.message, 'chore(release):')" - # name: spend tests against network + # name: transaction tests against network # runs-on: ${{ matrix.os }} # strategy: # matrix: @@ -607,14 +603,6 @@ jobs: # run: cargo build --release --bin faucet --features="local,gifting" # timeout-minutes: 30 - # - name: Build testing executable - # run: cargo test --release -p sn_node --features=local --test sequential_transfers --test storage_payments --test double_spend --no-run - # env: - # # only set the target dir for windows to bypass the linker issue. - # # happens if we build the node manager via testnet action - # CARGO_TARGET_DIR: ${{ matrix.os == 'windows-latest' && './test-target' || '.' }} - # timeout-minutes: 30 - # - name: Start a local network # uses: maidsafe/sn-local-testnet-action@main # with: @@ -649,24 +637,18 @@ jobs: # CARGO_TARGET_DIR: ${{ matrix.os == 'windows-latest' && './test-target' || '.' }} # timeout-minutes: 25 - # - name: execute the double spend tests - # run: cargo test --release -p sn_node --features="local" --test double_spend -- --nocapture --test-threads=1 - # env: - # CARGO_TARGET_DIR: ${{ matrix.os == 'windows-latest' && './test-target' || '.' }} - # timeout-minutes: 25 - # - name: Stop the local network and upload logs # if: always() # uses: maidsafe/sn-local-testnet-action@main # with: # action: stop - # log_file_prefix: safe_test_logs_spend + # log_file_prefix: safe_test_logs_transaction # platform: ${{ matrix.os }} # # runs with increased node count - # spend_simulation: + # transaction_simulation: # if: "!startsWith(github.event.head_commit.message, 'chore(release):')" - # name: spend simulation + # name: transaction simulation # runs-on: ${{ matrix.os }} # strategy: # matrix: @@ -688,7 +670,7 @@ jobs: # timeout-minutes: 30 # - name: Build testing executable - # run: cargo test --release -p sn_node --features=local --test spend_simulation --no-run + # run: cargo test --release -p sn_node --features=local --test transaction_simulation --no-run # env: # # only set the target dir for windows to bypass the linker issue. # # happens if we build the node manager via testnet action @@ -716,8 +698,8 @@ jobs: # echo "SAFE_PEERS has been set to $SAFE_PEERS" # fi - # - name: execute the spend simulation - # run: cargo test --release -p sn_node --features="local" --test spend_simulation -- --nocapture + # - name: execute the transaction simulation + # run: cargo test --release -p sn_node --features="local" --test transaction_simulation -- --nocapture # env: # CARGO_TARGET_DIR: ${{ matrix.os == 'windows-latest' && './test-target' || '.' }} # timeout-minutes: 25 @@ -727,7 +709,7 @@ jobs: # uses: maidsafe/sn-local-testnet-action@main # with: # action: stop - # log_file_prefix: safe_test_logs_spend_simulation + # log_file_prefix: safe_test_logs_transaction_simulation # platform: ${{ matrix.os }} # token_distribution_test: @@ -1502,18 +1484,18 @@ jobs: # SN_LOG: "all" # timeout-minutes: 5 - # - name: Ensure no leftover cash_notes and payment files + # - name: Ensure no leftover transactions and payment files # run: | - # expected_cash_notes_files="1" + # expected_transactions_files="1" # expected_payment_files="0" # pwd # ls $CLIENT_DATA_PATH/ -l # ls $CLIENT_DATA_PATH/wallet -l - # ls $CLIENT_DATA_PATH/wallet/cash_notes -l - # cash_note_files=$(ls $CLIENT_DATA_PATH/wallet/cash_notes | wc -l) - # echo "Find $cash_note_files cash_note files" - # if [ $expected_cash_notes_files -lt $cash_note_files ]; then - # echo "Got too many cash_note files leftover: $cash_note_files" + # ls $CLIENT_DATA_PATH/wallet/transactions -l + # transaction_files=$(ls $CLIENT_DATA_PATH/wallet/transactions | wc -l) + # echo "Find $transaction_files transaction files" + # if [ $expected_transactions_files -lt $transaction_files ]; then + # echo "Got too many transaction files leftover: $transaction_files" # exit 1 # fi # ls $CLIENT_DATA_PATH/wallet/payments -l @@ -1536,17 +1518,17 @@ jobs: # SN_LOG: "all" # timeout-minutes: 10 - # - name: Ensure no leftover cash_notes and payment files + # - name: Ensure no leftover transactions and payment files # run: | - # expected_cash_notes_files="1" + # expected_transactions_files="1" # expected_payment_files="0" # pwd # ls $CLIENT_DATA_PATH/ -l # ls $CLIENT_DATA_PATH/wallet -l - # ls $CLIENT_DATA_PATH/wallet/cash_notes -l - # cash_note_files=$(find $CLIENT_DATA_PATH/wallet/cash_notes -type f | wc -l) - # if (( $(echo "$cash_note_files > $expected_cash_notes_files" | bc -l) )); then - # echo "Got too many cash_note files leftover: $cash_note_files when we expected $expected_cash_notes_files" + # ls $CLIENT_DATA_PATH/wallet/transactions -l + # transaction_files=$(find $CLIENT_DATA_PATH/wallet/transactions -type f | wc -l) + # if (( $(echo "$transaction_files > $expected_transactions_files" | bc -l) )); then + # echo "Got too many transaction files leftover: $transaction_files when we expected $expected_transactions_files" # exit 1 # fi # ls $CLIENT_DATA_PATH/wallet/payments -l @@ -1589,18 +1571,18 @@ jobs: # SN_LOG: "all" # timeout-minutes: 10 - # - name: Ensure no leftover cash_notes and payment files + # - name: Ensure no leftover transactions and payment files # run: | - # expected_cash_notes_files="1" + # expected_transactions_files="1" # expected_payment_files="0" # pwd # ls $CLIENT_DATA_PATH/ -l # ls $CLIENT_DATA_PATH/wallet -l - # ls $CLIENT_DATA_PATH/wallet/cash_notes -l - # cash_note_files=$(ls $CLIENT_DATA_PATH/wallet/cash_notes | wc -l) - # echo "Find $cash_note_files cash_note files" - # if [ $expected_cash_notes_files -lt $cash_note_files ]; then - # echo "Got too many cash_note files leftover: $cash_note_files" + # ls $CLIENT_DATA_PATH/wallet/transactions -l + # transaction_files=$(ls $CLIENT_DATA_PATH/wallet/transactions | wc -l) + # echo "Find $transaction_files transaction files" + # if [ $expected_transactions_files -lt $transaction_files ]; then + # echo "Got too many transaction files leftover: $transaction_files" # exit 1 # fi # ls $CLIENT_DATA_PATH/wallet/payments -l diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index a1e0ef2046..ca6058bd72 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -256,10 +256,6 @@ jobs: timeout-minutes: 25 run: cargo test --release --package sn_protocol - - name: Run transfers tests - timeout-minutes: 25 - run: cargo test --release --package sn_transfers - - name: Run logging tests timeout-minutes: 25 run: cargo test --release --package sn_logging diff --git a/.github/workflows/nightly_wan.yml b/.github/workflows/nightly_wan.yml index 681a45e625..7cdcecdcb5 100644 --- a/.github/workflows/nightly_wan.yml +++ b/.github/workflows/nightly_wan.yml @@ -147,7 +147,7 @@ jobs: SLACK_MESSAGE: "Please check the logs for the run at ${{ env.WORKFLOW_URL }}/${{ github.run_id }}" SLACK_TITLE: "Nightly E2E Test Run Failed" - # spend_test: + # transaction_test: # name: Spend tests against network # runs-on: ${{ matrix.os }} # strategy: @@ -162,10 +162,6 @@ jobs: # - uses: Swatinem/rust-cache@v2 # continue-on-error: true - # - name: Build testing executable - # run: cargo test --release -p sn_node --features=local --test sequential_transfers --test storage_payments --test double_spend --test spend_simulation --no-run - # timeout-minutes: 40 - # - name: setup testnet-deploy # uses: maidsafe/sn-testnet-control-action/init-testnet-deploy@main # with: @@ -208,14 +204,6 @@ jobs: # SN_LOG: "all" # timeout-minutes: 45 - # - name: execute the double spend tests - # run: cargo test --release -p sn_node --test double_spend -- --nocapture --test-threads=1 - # timeout-minutes: 45 - - # - name: execute the spend simulation tests - # run: cargo test --release -p sn_node --test spend_simulation -- --nocapture --test-threads=1 - # timeout-minutes: 45 - # - name: Small wait to allow reward receipt # run: sleep 30 # timeout-minutes: 1 diff --git a/Cargo.lock b/Cargo.lock index acc3de7f49..46b795128b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2071,15 +2071,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "cpp_demangle" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" -dependencies = [ - "cfg-if", -] - [[package]] name = "cpufeatures" version = "0.2.14" @@ -2440,15 +2431,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - [[package]] name = "der" version = "0.6.1" @@ -3036,18 +3018,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "findshlibs" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" -dependencies = [ - "cc", - "lazy_static", - "libc", - "winapi", -] - [[package]] name = "fixed-hash" version = "0.8.0" @@ -3133,16 +3103,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "fs_extra" version = "1.3.0" @@ -4578,24 +4538,6 @@ version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" -[[package]] -name = "inferno" -version = "0.11.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" -dependencies = [ - "ahash", - "indexmap 2.5.0", - "is-terminal", - "itoa", - "log", - "num-format", - "once_cell", - "quick-xml 0.26.0", - "rgb", - "str_stack", -] - [[package]] name = "inout" version = "0.1.3" @@ -5685,17 +5627,6 @@ dependencies = [ "libc", ] -[[package]] -name = "nix" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.27.1" @@ -5829,16 +5760,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec", - "itoa", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -6452,7 +6373,7 @@ checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", "indexmap 2.5.0", - "quick-xml 0.32.0", + "quick-xml", "serde", "time", ] @@ -6548,27 +6469,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" -[[package]] -name = "pprof" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5c97c51bd34c7e742402e216abdeb44d415fbe6ae41d56b114723e953711cb" -dependencies = [ - "backtrace", - "cfg-if", - "findshlibs", - "inferno", - "libc", - "log", - "nix 0.26.4", - "once_cell", - "parking_lot", - "smallvec", - "symbolic-demangle", - "tempfile", - "thiserror", -] - [[package]] name = "ppv-lite86" version = "0.2.20" @@ -6924,15 +6824,6 @@ dependencies = [ "unsigned-varint 0.8.0", ] -[[package]] -name = "quick-xml" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.32.0" @@ -7467,15 +7358,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "rgb" -version = "0.8.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" -dependencies = [ - "bytemuck", -] - [[package]] name = "ring" version = "0.16.20" @@ -7903,15 +7785,6 @@ dependencies = [ "cc", ] -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "zeroize", -] - [[package]] name = "self_encryption" version = "0.30.0" @@ -8320,7 +8193,6 @@ dependencies = [ "sn_peers_acquisition", "sn_protocol", "sn_service_management", - "sn_transfers", "sysinfo", "thiserror", "tokio", @@ -8500,7 +8372,6 @@ dependencies = [ "sn_evm", "sn_protocol", "sn_registers", - "sn_transfers", "strum", "sysinfo", "thiserror", @@ -8557,7 +8428,6 @@ dependencies = [ "sn_protocol", "sn_registers", "sn_service_management", - "sn_transfers", "strum", "sysinfo", "tempfile", @@ -8591,7 +8461,6 @@ dependencies = [ "sn_peers_acquisition", "sn_protocol", "sn_service_management", - "sn_transfers", "thiserror", "tokio", "tokio-stream", @@ -8638,7 +8507,6 @@ dependencies = [ "sn_build_info", "sn_evm", "sn_registers", - "sn_transfers", "thiserror", "tiny-keccak", "tonic 0.6.2", @@ -8690,39 +8558,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "sn_transfers" -version = "0.20.3" -dependencies = [ - "assert_fs", - "blsttc", - "chrono", - "criterion", - "custom_debug", - "dirs-next", - "eyre", - "fs2", - "hex 0.4.3", - "lazy_static", - "libp2p", - "pprof", - "rand 0.8.5", - "rayon", - "ring 0.17.8", - "rmp-serde", - "secrecy", - "serde", - "serde_bytes", - "serde_json", - "tempfile", - "thiserror", - "tiny-keccak", - "tokio", - "tracing", - "walkdir", - "xor_name", -] - [[package]] name = "snow" version = "0.9.6" @@ -8797,24 +8632,12 @@ dependencies = [ "der 0.7.9", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str_stack" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" - [[package]] name = "strip-ansi-escapes" version = "0.2.0" @@ -8858,29 +8681,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "symbolic-common" -version = "12.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fdf97c441f18a4f92425b896a4ec7a27e03631a0b1047ec4e34e9916a9a167e" -dependencies = [ - "debugid", - "memmap2", - "stable_deref_trait", - "uuid", -] - -[[package]] -name = "symbolic-demangle" -version = "12.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8ece6b129e97e53d1fbb3f61d33a6a9e5369b11d01228c068094d6d134eaea" -dependencies = [ - "cpp_demangle", - "rustc-demangle", - "symbolic-common", -] - [[package]] name = "syn" version = "1.0.109" diff --git a/Cargo.toml b/Cargo.toml index 888d541c75..a7b76bca0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ members = [ "sn_protocol", "sn_registers", "sn_service_management", - "sn_transfers", "test_utils", "token_supplies", ] diff --git a/README.md b/README.md index 7de1c13080..1826f71142 100644 --- a/README.md +++ b/README.md @@ -99,8 +99,6 @@ WASM support for the autonomi API is currently under active development. More do networking layer, built atop libp2p which allows nodes and clients to communicate. - [Protocol](https://github.com/maidsafe/safe_network/blob/main/sn_protocol/README.md) The protocol used by the safe network. -- [Transfers](https://github.com/maidsafe/safe_network/blob/main/sn_transfers/README.md) The - transfers crate, used to send and receive tokens Native to the network. - [Registers](https://github.com/maidsafe/safe_network/blob/main/sn_registers/README.md) The registers crate, used for the Register CRDT data type on the network. - [Peers Acquisition](https://github.com/maidsafe/safe_network/blob/main/sn_peers_acquisition/README.md) diff --git a/autonomi/examples/metamask/index.js b/autonomi/examples/metamask/index.js index 66bf524037..235fa9f8c6 100644 --- a/autonomi/examples/metamask/index.js +++ b/autonomi/examples/metamask/index.js @@ -189,16 +189,16 @@ const executeQuotePayments = async (sender, quotes, quotePayments) => { quotePayments ); - // Form approve to spend tokens calldata + // Form approve to transaction tokens calldata const approveCalldata = autonomi.getApproveToSpendTokensCalldata( evmNetwork, - payForQuotesCalldata.approve_spender, + payForQuotesCalldata.approve_transactioner, payForQuotesCalldata.approve_amount ); console.log("Sending approve transaction.."); - // Approve to spend tokens + // Approve to transaction tokens let hash = await sendTransaction({ from: sender, to: approveCalldata[1], diff --git a/autonomi/src/client/wasm.rs b/autonomi/src/client/wasm.rs index 6c3a151135..cc0e03155a 100644 --- a/autonomi/src/client/wasm.rs +++ b/autonomi/src/client/wasm.rs @@ -637,7 +637,9 @@ mod external_signer { use crate::client::external_signer::encrypt_data; use crate::client::payment::Receipt; use crate::receipt_from_quotes_and_payments; - use sn_evm::external_signer::{approve_to_spend_tokens_calldata, pay_for_quotes_calldata}; + use sn_evm::external_signer::{ + approve_to_transaction_tokens_calldata, pay_for_quotes_calldata, + }; use sn_evm::EvmNetwork; use sn_evm::QuotePayment; use sn_evm::{Amount, PaymentQuote}; @@ -740,17 +742,17 @@ mod external_signer { Ok(js_value) } - /// Form approve to spend tokens calldata. + /// Form approve to transaction tokens calldata. #[wasm_bindgen(js_name = getApproveToSpendTokensCalldata)] - pub fn get_approve_to_spend_tokens_calldata( + pub fn get_approve_to_transaction_tokens_calldata( network: JsValue, - spender: JsValue, + transactioner: JsValue, amount: JsValue, ) -> Result { let network: EvmNetwork = serde_wasm_bindgen::from_value(network)?; - let spender: EvmAddress = serde_wasm_bindgen::from_value(spender)?; + let transactioner: EvmAddress = serde_wasm_bindgen::from_value(transactioner)?; let amount: Amount = serde_wasm_bindgen::from_value(amount)?; - let calldata = approve_to_spend_tokens_calldata(&network, spender, amount); + let calldata = approve_to_transaction_tokens_calldata(&network, transactioner, amount); let js_value = serde_wasm_bindgen::to_value(&calldata)?; Ok(js_value) } diff --git a/autonomi/tests/external_signer.rs b/autonomi/tests/external_signer.rs index 89c9cd4d48..4430ef519a 100644 --- a/autonomi/tests/external_signer.rs +++ b/autonomi/tests/external_signer.rs @@ -50,20 +50,21 @@ async fn pay_for_content_addresses( // Init an external wallet provider. In the webapp, this would be MetaMask for example let provider = wallet.to_provider(); - // Form approve to spend tokens transaction data - let approve_calldata = autonomi::client::external_signer::approve_to_spend_tokens_calldata( - wallet.network(), - pay_for_quotes_calldata.approve_spender, - pay_for_quotes_calldata.approve_amount, - ); - - // Prepare approve to spend tokens transaction + // Form approve to transaction tokens transaction data + let approve_calldata = + autonomi::client::external_signer::approve_to_transaction_tokens_calldata( + wallet.network(), + pay_for_quotes_calldata.approve_transactioner, + pay_for_quotes_calldata.approve_amount, + ); + + // Prepare approve to transaction tokens transaction let transaction_request = provider .transaction_request() .with_to(approve_calldata.1) .with_input(approve_calldata.0); - // Send approve to spend tokens transaction + // Send approve to transaction tokens transaction let _tx_hash = provider .send_transaction(transaction_request) .await? diff --git a/evmlib/artifacts/AutonomiNetworkToken.json b/evmlib/artifacts/AutonomiNetworkToken.json index 841ed5d678..0eac667117 100644 --- a/evmlib/artifacts/AutonomiNetworkToken.json +++ b/evmlib/artifacts/AutonomiNetworkToken.json @@ -60,7 +60,7 @@ "inputs": [ { "internalType": "address", - "name": "spender", + "name": "transactioner", "type": "address" }, { @@ -135,7 +135,7 @@ "inputs": [ { "internalType": "address", - "name": "spender", + "name": "transactioner", "type": "address" } ], @@ -261,7 +261,7 @@ { "indexed": true, "internalType": "address", - "name": "spender", + "name": "transactioner", "type": "address" }, { @@ -390,7 +390,7 @@ }, { "internalType": "address", - "name": "spender", + "name": "transactioner", "type": "address" } ], @@ -409,7 +409,7 @@ "inputs": [ { "internalType": "address", - "name": "spender", + "name": "transactioner", "type": "address" }, { @@ -776,7 +776,7 @@ }, { "internalType": "address", - "name": "spender", + "name": "transactioner", "type": "address" }, { diff --git a/evmlib/src/contract/network_token.rs b/evmlib/src/contract/network_token.rs index 10903c9fd2..0ccf9c84d6 100644 --- a/evmlib/src/contract/network_token.rs +++ b/evmlib/src/contract/network_token.rs @@ -76,23 +76,23 @@ where } /// See how many tokens are approved to be spent. - pub async fn allowance(&self, owner: Address, spender: Address) -> Result { - debug!("Getting allowance of owner: {owner} for spender: {spender}",); + pub async fn allowance(&self, owner: Address, transactioner: Address) -> Result { + debug!("Getting allowance of owner: {owner} for transactioner: {transactioner}",); let balance = self .contract - .allowance(owner, spender) + .allowance(owner, transactioner) .call() .await .inspect_err(|err| error!("Error getting allowance: {err:?}"))? ._0; - debug!("Allowance of owner: {owner} for spender: {spender} is: {balance}"); + debug!("Allowance of owner: {owner} for transactioner: {transactioner} is: {balance}"); Ok(balance) } - /// Approve spender to spend a raw amount of tokens. - pub async fn approve(&self, spender: Address, value: U256) -> Result { - debug!("Approving spender to spend raw amt of tokens: {value}"); - let (calldata, to) = self.approve_calldata(spender, value); + /// Approve transactioner to transaction a raw amount of tokens. + pub async fn approve(&self, transactioner: Address, value: U256) -> Result { + debug!("Approving transactioner to transaction raw amt of tokens: {value}"); + let (calldata, to) = self.approve_calldata(transactioner, value); let transaction_request = self .contract @@ -108,13 +108,13 @@ where .await .inspect_err(|err| { error!( - "Error approving spender {spender:?} to spend raw amt of tokens {value}: {err:?}" + "Error approving transactioner {transactioner:?} to transaction raw amt of tokens {value}: {err:?}" ) })?; let pending_tx_hash = *pending_tx_builder.tx_hash(); - debug!("The approval from sender {spender:?} is pending with tx_hash: {pending_tx_hash:?}",); + debug!("The approval from sender {transactioner:?} is pending with tx_hash: {pending_tx_hash:?}",); let tx_hash = pending_tx_builder.watch().await.inspect_err(|err| { error!("Error watching approve tx with hash {pending_tx_hash:?}: {err:?}") @@ -125,10 +125,14 @@ where Ok(tx_hash) } - /// Approve spender to spend a raw amount of tokens. + /// Approve transactioner to transaction a raw amount of tokens. /// Returns the transaction calldata. - pub fn approve_calldata(&self, spender: Address, value: U256) -> (Calldata, Address) { - let calldata = self.contract.approve(spender, value).calldata().to_owned(); + pub fn approve_calldata(&self, transactioner: Address, value: U256) -> (Calldata, Address) { + let calldata = self + .contract + .approve(transactioner, value) + .calldata() + .to_owned(); (calldata, *self.contract.address()) } diff --git a/evmlib/src/external_signer.rs b/evmlib/src/external_signer.rs index 20c3aa95df..545c26c1df 100644 --- a/evmlib/src/external_signer.rs +++ b/evmlib/src/external_signer.rs @@ -23,17 +23,17 @@ pub enum Error { DataPaymentsContract(#[from] data_payments::error::Error), } -/// Approve an address / smart contract to spend this wallet's payment tokens. +/// Approve an address / smart contract to transaction this wallet's payment tokens. /// /// Returns the transaction calldata (input, to). -pub fn approve_to_spend_tokens_calldata( +pub fn approve_to_transaction_tokens_calldata( network: &Network, - spender: Address, + transactioner: Address, value: U256, ) -> (Calldata, Address) { let provider = http_provider(network.rpc_url().clone()); let network_token = NetworkToken::new(*network.payment_token_address(), provider); - network_token.approve_calldata(spender, value) + network_token.approve_calldata(transactioner, value) } /// Transfer payment tokens from the supplied wallet to an address. @@ -53,14 +53,14 @@ pub fn transfer_tokens_calldata( pub struct PayForQuotesCalldataReturnType { pub batched_calldata_map: HashMap>, pub to: Address, - pub approve_spender: Address, + pub approve_transactioner: Address, pub approve_amount: Amount, } /// Use this wallet to pay for chunks in batched transfer transactions. /// If the amount of transfers is more than one transaction can contain, the transfers will be split up over multiple transactions. /// -/// Returns PayForQuotesCalldataReturnType, containing calldata of the transaction batches along with the approval details for the spender. +/// Returns PayForQuotesCalldataReturnType, containing calldata of the transaction batches along with the approval details for the transactioner. pub fn pay_for_quotes_calldata>( network: &Network, payments: T, @@ -69,7 +69,7 @@ pub fn pay_for_quotes_calldata>( let total_amount = payments.iter().map(|(_, _, amount)| amount).sum(); - let approve_spender = *network.data_payments_address(); + let approve_transactioner = *network.data_payments_address(); let approve_amount = total_amount; let provider = http_provider(network.rpc_url().clone()); @@ -90,7 +90,7 @@ pub fn pay_for_quotes_calldata>( Ok(PayForQuotesCalldataReturnType { batched_calldata_map: calldata_map, to: *data_payments.contract.address(), - approve_spender, + approve_transactioner, approve_amount, }) } diff --git a/evmlib/src/wallet.rs b/evmlib/src/wallet.rs index 643d14bdf9..04365ce593 100644 --- a/evmlib/src/wallet.rs +++ b/evmlib/src/wallet.rs @@ -105,18 +105,22 @@ impl Wallet { transfer_gas_tokens(self.wallet.clone(), &self.network, to, amount).await } - /// See how many tokens of the owner may be spent by the spender. - pub async fn token_allowance(&self, spender: Address) -> Result { - token_allowance(&self.network, self.address(), spender).await + /// See how many tokens of the owner may be spent by the transactioner. + pub async fn token_allowance( + &self, + transactioner: Address, + ) -> Result { + token_allowance(&self.network, self.address(), transactioner).await } - /// Approve an address / smart contract to spend this wallet's payment tokens. - pub async fn approve_to_spend_tokens( + /// Approve an address / smart contract to transaction this wallet's payment tokens. + pub async fn approve_to_transaction_tokens( &self, - spender: Address, + transactioner: Address, amount: U256, ) -> Result { - approve_to_spend_tokens(self.wallet.clone(), &self.network, spender, amount).await + approve_to_transaction_tokens(self.wallet.clone(), &self.network, transactioner, amount) + .await } /// Pays for a single quote. Returns transaction hash of the payment. @@ -223,29 +227,29 @@ pub async fn balance_of_gas_tokens( Ok(balance) } -/// See how many tokens of the owner may be spent by the spender. +/// See how many tokens of the owner may be spent by the transactioner. pub async fn token_allowance( network: &Network, owner: Address, - spender: Address, + transactioner: Address, ) -> Result { - debug!("Getting allowance for owner: {owner} and spender: {spender}",); + debug!("Getting allowance for owner: {owner} and transactioner: {transactioner}",); let provider = http_provider(network.rpc_url().clone()); let network_token = NetworkToken::new(*network.payment_token_address(), provider); - network_token.allowance(owner, spender).await + network_token.allowance(owner, transactioner).await } -/// Approve an address / smart contract to spend this wallet's payment tokens. -pub async fn approve_to_spend_tokens( +/// Approve an address / smart contract to transaction this wallet's payment tokens. +pub async fn approve_to_transaction_tokens( wallet: EthereumWallet, network: &Network, - spender: Address, + transactioner: Address, amount: U256, ) -> Result { - debug!("Approving address/smart contract with {amount} tokens at address: {spender}",); + debug!("Approving address/smart contract with {amount} tokens at address: {transactioner}",); let provider = http_provider_with_wallet(network.rpc_url().clone(), wallet); let network_token = NetworkToken::new(*network.payment_token_address(), provider); - network_token.approve(spender, amount).await + network_token.approve(transactioner, amount).await } /// Transfer payment tokens from the supplied wallet to an address. @@ -319,8 +323,8 @@ pub async fn pay_for_quotes>( // TODO: Get rid of approvals altogether, by using permits or whatever.. if allowance < total_amount_to_be_paid { - // Approve the contract to spend all the client's tokens. - approve_to_spend_tokens( + // Approve the contract to transaction all the client's tokens. + approve_to_transaction_tokens( wallet.clone(), network, *network.data_payments_address(), diff --git a/evmlib/tests/network_token.rs b/evmlib/tests/network_token.rs index 0cc2b1c1eb..6a703168ec 100644 --- a/evmlib/tests/network_token.rs +++ b/evmlib/tests/network_token.rs @@ -70,11 +70,13 @@ async fn test_approve() { let account = wallet_address(network_token.contract.provider().wallet()); - let spend_value = U256::from(1); - let spender = PrivateKeySigner::random(); + let transaction_value = U256::from(1); + let transactioner = PrivateKeySigner::random(); - // Approve for the spender to spend a value from the funds of the owner (our default account). - let approval_result = network_token.approve(spender.address(), spend_value).await; + // Approve for the transactioner to transaction a value from the funds of the owner (our default account). + let approval_result = network_token + .approve(transactioner.address(), transaction_value) + .await; assert!( approval_result.is_ok(), @@ -84,11 +86,11 @@ async fn test_approve() { let allowance = network_token .contract - .allowance(account, spender.address()) + .allowance(account, transactioner.address()) .call() .await .unwrap() ._0; - assert_eq!(allowance, spend_value); + assert_eq!(allowance, transaction_value); } diff --git a/node-launchpad/.config/config.json5 b/node-launchpad/.config/config.json5 index 63786942ce..dc208acbda 100644 --- a/node-launchpad/.config/config.json5 +++ b/node-launchpad/.config/config.json5 @@ -31,7 +31,7 @@ "": "Quit", "": "Quit", "": "Quit", - "": "Suspend" // Suspend the application + "": "Sutransaction" // Sutransaction the application }, "Options": { "": {"SwitchScene":"Status"}, @@ -67,7 +67,7 @@ "": "Quit", "": "Quit", "": "Quit", - "": "Suspend" // Suspend the application + "": "Sutransaction" // Sutransaction the application }, "Help": { "": {"SwitchScene":"Status"}, @@ -82,7 +82,7 @@ "": "Quit", "": "Quit", "": "Quit", - "": "Suspend" // Suspend the application + "": "Sutransaction" // Sutransaction the application } } } diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index 5f4669a4d7..c5f8a0fb08 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -32,7 +32,7 @@ pub enum Action { Tick, Render, Resize(u16, u16), - Suspend, + Sutransaction, Resume, Quit, Refresh, diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index dac3f1e4a3..26ad1029e9 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -42,7 +42,7 @@ pub struct App { pub frame_rate: f64, pub components: Vec>, pub should_quit: bool, - pub should_suspend: bool, + pub should_sutransaction: bool, pub input_mode: InputMode, pub scene: Scene, pub last_tick_key_events: Vec, @@ -150,7 +150,7 @@ impl App { Box::new(upgrade_nodes), ], should_quit: false, - should_suspend: false, + should_sutransaction: false, input_mode: InputMode::Navigation, scene: Scene::Status, last_tick_key_events: Vec::new(), @@ -221,8 +221,8 @@ impl App { self.last_tick_key_events.drain(..); } Action::Quit => self.should_quit = true, - Action::Suspend => self.should_suspend = true, - Action::Resume => self.should_suspend = false, + Action::Sutransaction => self.should_sutransaction = true, + Action::Resume => self.should_sutransaction = false, Action::Resize(w, h) => { tui.resize(Rect::new(0, 0, w, h))?; tui.draw(|f| { @@ -296,8 +296,8 @@ impl App { }; } } - if self.should_suspend { - tui.suspend()?; + if self.should_sutransaction { + tui.sutransaction()?; action_tx.send(Action::Resume)?; tui = tui::Tui::new()? .tick_rate(self.tick_rate) diff --git a/node-launchpad/src/tui.rs b/node-launchpad/src/tui.rs index 32fe8bfc42..41a25b31a6 100644 --- a/node-launchpad/src/tui.rs +++ b/node-launchpad/src/tui.rs @@ -219,7 +219,7 @@ impl Tui { self.cancellation_token.cancel(); } - pub fn suspend(&mut self) -> Result<()> { + pub fn sutransaction(&mut self) -> Result<()> { self.exit()?; #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; diff --git a/sn_auditor/CHANGELOG.md b/sn_auditor/CHANGELOG.md deleted file mode 100644 index 60a90a181d..0000000000 --- a/sn_auditor/CHANGELOG.md +++ /dev/null @@ -1,137 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.1.24](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.23...sn_auditor-v0.1.24) - 2024-06-04 - -### Fixed -- *(audit)* dont overwrite beta tracking payments - -## [0.1.23](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.22...sn_auditor-v0.1.23) - 2024-06-04 - -### Other -- updated the following local packages: sn_client - -## [0.1.22](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.21...sn_auditor-v0.1.22) - 2024-06-04 - -### Added -- utxo reattempt by env - -## [0.1.21](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.20...sn_auditor-v0.1.21) - 2024-06-04 - -### Other -- reduce dag recrawl interval - -## [0.1.20](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.19...sn_auditor-v0.1.20) - 2024-06-03 - -### Other -- updated the following local packages: sn_client - -## [0.1.19](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.18...sn_auditor-v0.1.19) - 2024-06-03 - -### Other -- bump versions to enable re-release with env vars at compilation - -## [0.1.18](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.17...sn_auditor-v0.1.18) - 2024-06-03 - -### Added -- *(auditor)* measuring beta tracking performance -- integrate DAG crawling fixes from Josh and Qi - -### Fixed -- *(auditor)* check unknown hash when add new participant - -### Other -- spend verification error management - -## [0.1.17](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.16...sn_auditor-v0.1.17) - 2024-05-24 - -### Added -- *(auditor)* cache beta participants to the disk -- *(auditor)* add new beta participants via endpoint -- backup rewards json to disk regularly -- docs for sn_auditor -- offline mode for beta rewards -- upgrade cli audit to use DAG -- *(audit)* simplify reward output -- *(audit)* make svg processing a non-deafult feat -- *(audit)* accept line separated list of discord ids -- remove two uneeded env vars -- pass genesis_cn pub fields separate to hide sk -- pass sk_str via cli opt -- improve code to use existing utils -- tracking beta rewards from the DAG -- dag faults unit tests, sn_auditor offline mode - -### Fixed -- *(auditor)* discord id cannot be empty -- *(auditor)* extend the beta particpants list -- auditor key arg to match docs -- dag and dag-svg feature mismatch -- beta rewards participants overwriting and renamings -- allow unknown discord IDs temporarily -- orphan parent bug, improve fault detection and logging - -### Other -- move dag svg -- rename improperly named foundation_key -- *(release)* sn_auditor-v0.1.16/sn_cli-v0.91.4/sn_faucet-v0.4.18/sn_metrics-v0.1.7/sn_node-v0.106.4/sn_service_management-v0.2.8/node-launchpad-v0.1.5/sn-node-manager-v0.7.7/sn_node_rpc_client-v0.6.17 -- *(release)* sn_auditor-v0.1.15/sn_cli-v0.91.3/sn_faucet-v0.4.17/sn_metrics-v0.1.6/sn_node-v0.106.3/sn_service_management-v0.2.7/node-launchpad-v0.1.2/sn_node_rpc_client-v0.6.16 -- *(release)* sn_client-v0.106.2/sn_networking-v0.15.2/sn_cli-v0.91.2/sn_node-v0.106.2/sn_auditor-v0.1.14/sn_faucet-v0.4.16/sn_node_rpc_client-v0.6.15 -- *(release)* sn_auditor-v0.1.13/sn_client-v0.106.1/sn_networking-v0.15.1/sn_protocol-v0.16.6/sn_cli-v0.91.1/sn_faucet-v0.4.15/sn_node-v0.106.1/node-launchpad-v0.1.1/sn_node_rpc_client-v0.6.14/sn_peers_acquisition-v0.2.12/sn_service_management-v0.2.6 -- *(release)* sn_auditor-v0.1.12/sn_client-v0.106.0/sn_networking-v0.15.0/sn_transfers-v0.18.0/sn_peers_acquisition-v0.2.11/sn_logging-v0.2.26/sn_cli-v0.91.0/sn_faucet-v0.4.14/sn_metrics-v0.1.5/sn_node-v0.106.0/sn_service_management-v0.2.5/test_utils-v0.4.1/node-launchpad-v/sn-node-manager-v0.7.5/sn_node_rpc_client-v0.6.13/token_supplies-v0.1.48/sn_protocol-v0.16.5 -- *(versions)* sync versions with latest crates.io vs -- *(release)* sn_auditor-v0.1.7/sn_client-v0.105.3/sn_networking-v0.14.4/sn_protocol-v0.16.3/sn_build_info-v0.1.7/sn_transfers-v0.17.2/sn_peers_acquisition-v0.2.10/sn_cli-v0.90.4/sn_faucet-v0.4.9/sn_metrics-v0.1.4/sn_node-v0.105.6/sn_service_management-v0.2.4/sn-node-manager-v0.7.4/sn_node_rpc_client-v0.6.8/token_supplies-v0.1.47 -- *(deps)* bump dependencies - -## [0.1.16](https://github.com/maidsafe/safe_network/compare/sn_auditor-v0.1.15...sn_auditor-v0.1.16) - 2024-05-20 - -### Other -- update Cargo.lock dependencies - -## [0.1.15](https://github.com/maidsafe/safe_network/compare/sn_auditor-v0.1.14...sn_auditor-v0.1.15) - 2024-05-15 - -### Other -- update Cargo.lock dependencies - -## [0.1.14](https://github.com/maidsafe/safe_network/compare/sn_auditor-v0.1.13...sn_auditor-v0.1.14) - 2024-05-09 - -### Other -- updated the following local packages: sn_client - -## [0.1.13](https://github.com/maidsafe/safe_network/compare/sn_auditor-v0.1.12...sn_auditor-v0.1.13) - 2024-05-08 - -### Other -- update Cargo.lock dependencies - -## [0.1.12-alpha.1](https://github.com/maidsafe/safe_network/compare/sn_auditor-v0.1.12-alpha.0...sn_auditor-v0.1.12-alpha.1) - 2024-05-07 - -### Other -- update Cargo.lock dependencies - -## [0.1.2](https://github.com/maidsafe/safe_network/compare/sn_auditor-v0.1.1...sn_auditor-v0.1.2) - 2024-03-28 - -### Other -- updated the following local packages: sn_client - -## [0.1.1](https://github.com/joshuef/safe_network/compare/sn_auditor-v0.1.0...sn_auditor-v0.1.1) - 2024-03-28 - -### Other -- updated the following local packages: sn_client - -## [0.1.0](https://github.com/joshuef/safe_network/releases/tag/sn_auditor-v0.1.0) - 2024-03-27 - -### Added -- svg caching, fault tolerance during DAG collection -- make logging simpler to use -- introducing sn_auditor - -### Fixed -- logging, adapt program name - -### Other -- remove Cargo.lock diff --git a/sn_auditor/Cargo.toml b/sn_auditor/Cargo.toml deleted file mode 100644 index f89d345672..0000000000 --- a/sn_auditor/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -authors = ["MaidSafe Developers "] -description = "Safe Network Auditor" -name = "sn_auditor" -version = "0.3.5" -edition = "2021" -homepage = "https://maidsafe.net" -repository = "https://github.com/maidsafe/safe_network" -license = "GPL-3.0" -readme = "README.md" - -[features] -default = [] -local = ["sn_client/local", "sn_peers_acquisition/local"] -network-contacts = ["sn_peers_acquisition/network-contacts"] -nightly = [] -open-metrics = ["sn_client/open-metrics"] -websockets = ["sn_client/websockets"] -svg-dag = ["graphviz-rust", "dag-collection"] -dag-collection = [] - -[dependencies] -bls = { package = "blsttc", version = "8.0.1" } -clap = { version = "4.2.1", features = ["derive"] } -color-eyre = "~0.6" -dirs-next = "~2.0.0" -futures = "0.3.28" -graphviz-rust = { version = "0.9.0", optional = true } -lazy_static = "1.4.0" -serde = { version = "1.0.133", features = ["derive", "rc"] } -serde_json = "1.0.108" -sn_build_info = { path = "../sn_build_info", version = "0.1.15" } -sn_client = { path = "../sn_client", version = "0.110.4" } -sn_logging = { path = "../sn_logging", version = "0.2.36" } -sn_peers_acquisition = { path = "../sn_peers_acquisition", version = "0.5.3" } -sn_protocol = { path = "../sn_protocol", version = "0.17.11" } -tiny_http = { version = "0.12", features = ["ssl-rustls"] } -tracing = { version = "~0.1.26" } -tokio = { version = "1.32.0", features = [ - "io-util", - "macros", - "parking_lot", - "rt", - "sync", - "time", - "fs", -] } -urlencoding = "2.1.3" - -[lints] -workspace = true diff --git a/sn_auditor/README.md b/sn_auditor/README.md deleted file mode 100644 index 1d8f96d59f..0000000000 --- a/sn_auditor/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# sn_auditor - -This is a small webserver application that allows you to audit the SAFE Network Currency by gathering a DAG of Spends on the Network. - -![](./resources/dag.svg) - -## Usage - -Running an auditor instance: - -```bash -# on a Network with known peers -cargo run --release --peer "/ip4/" - -# on a local testnet -cargo run --release --features=local -``` - -It can be run with the following flags: - -```bash - -f, --force-from-genesis - Force the spend DAG to be updated from genesis - - -c, --clean - Clear the local spend DAG and start from scratch - - -o, --offline-viewer - Visualize a local DAG file offline, does not connect to the Network - - -b, --beta-participants - Beta rewards program participants to track - Provide a file with a list of Discord - usernames as argument - - -k, --beta-encryption-key - Secret encryption key of the beta rewards to decypher - discord usernames of the beta participants -``` - -The following env var: - -``` -# time in seconds UTXOs are refetched in DAG crawl -UTXO_REATTEMPT_INTERVAL=3600 -``` - -## Endpoints - -The webserver listens on port `4242` and has the following endpoints: - -| route | description | -|-------------------|---------------------------------------------------| -|`"/"` | `svg` representation of the DAG | -|`"/spend/"` | `json` information about the spend at this `addr` | -|`"/beta-rewards"` | `json` list of beta rewards participants | - -Note that for the `"/"` endpoint to work properly you need: -- to have [graphviz](https://graphviz.org/download/) installed -- to enable the `svg-dag` feature flag (with `cargo run --release --features=svg-dag`) diff --git a/sn_auditor/resources/dag.svg b/sn_auditor/resources/dag.svg deleted file mode 100644 index 8bf6eb99df..0000000000 --- a/sn_auditor/resources/dag.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - -c1f1425c1823e48475b0828fca5d324e0c7941dcb52379174bcbedf5f9be3be5 - - -SpendAddress(c1f142) - - - - -e8f83f264e29fe515cb343c4dd54d8d4d9db750a6e57437867e33dd30869bead - - -SpendAddress(e8f83f) - - - - -0->1 - - -NanoTokens(900000000000000000) - - - -883e2d37b1fdf3f4cc3b889c8c8b904e369a699e32f64294bd3cc771825960af - - -SpendAddress(883e2d) - - - - -0->2 - - -NanoTokens(388490188500000000) - - - -66268051e972c408c5f27777d6ce080d609891194af303a19558da1c76fe271a - - -SpendAddress(662680) - - - - -1->4 - - -NanoTokens(899999999000000000) - - - -ae3b39145533d45758543c7409f3de7a972b1dddfe3ea18c7825df9bccf73739 - - -SpendAddress(ae3b39) - - - - -1->7 - - -NanoTokens(1000000000) - - - -964d04e290a8fd960b08d90aba03a5ea01ad88f7af5f917f0433b5e9271f30c1 - - -SpendAddress(964d04) - - - - -2->3 - - -NanoTokens(388490188500000000) - - - -6391d9cfbc43964587e1ebb049430e9038f3635d22aa407a046c88de55ddd9f3 - - -SpendAddress(6391d9) - - - - -4->5 - - -NanoTokens(1000000000) - - - -0b9e3253b87e1f75d65d53d9579980339b6016a2db3e0b24d82fd8728377d285 - - -SpendAddress(0b9e32) - - - - -4->6 - - -NanoTokens(899999998000000000) - - - diff --git a/sn_auditor/src/dag_db.rs b/sn_auditor/src/dag_db.rs deleted file mode 100644 index a21f64c94b..0000000000 --- a/sn_auditor/src/dag_db.rs +++ /dev/null @@ -1,796 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use bls::SecretKey; -#[cfg(feature = "svg-dag")] -use color_eyre::eyre::Context; -use color_eyre::eyre::{bail, eyre, Result}; -#[cfg(feature = "svg-dag")] -use graphviz_rust::{cmd::Format, exec, parse, printer::PrinterContext}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use sn_client::transfers::{ - Hash, NanoTokens, SignedSpend, SpendAddress, DEFAULT_PAYMENT_FORWARD_SK, -}; -use sn_client::transfers::{DEFAULT_NETWORK_ROYALTIES_PK, NETWORK_ROYALTIES_PK}; -use sn_client::{Client, SpendDag, SpendDagGet}; -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt::Write; -use std::path::PathBuf; -use std::sync::Arc; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::mpsc::Sender; -use tokio::sync::RwLock; - -pub const SPEND_DAG_FILENAME: &str = "spend_dag"; -#[cfg(feature = "svg-dag")] -pub const SPEND_DAG_SVG_FILENAME: &str = "spend_dag.svg"; -/// Store a locally copy to restore on restart -pub const BETA_PARTICIPANTS_FILENAME: &str = "beta_participants.txt"; - -lazy_static! { - /// time in seconds UTXOs are refetched in DAG crawl - static ref UTXO_REATTEMPT_SECONDS: u64 = std::env::var("UTXO_REATTEMPT_INTERVAL") - .unwrap_or("7200".to_string()) - .parse::() - .unwrap_or(7200); - - /// time in seconds UTXOs are refetched in DAG crawl - static ref UTXO_REATTEMPT_INTERVAL: Duration = Duration::from_secs(*UTXO_REATTEMPT_SECONDS); - - /// time in seconds to rest between DAG crawls - static ref DAG_CRAWL_REST_INTERVAL: Duration = Duration::from_secs( - std::env::var("DAG_CRAWL_REST_INTERVAL") - .unwrap_or("60".to_string()) - .parse::() - .unwrap_or(60) - ); -} - -const SPENDS_PROCESSING_BUFFER_SIZE: usize = 4096; - -/// Abstraction for the Spend DAG database -/// Currently in memory, with disk backup, but should probably be a real DB at scale -#[derive(Clone)] -pub struct SpendDagDb { - client: Option, - pub(crate) path: PathBuf, - dag: Arc>, - beta_tracking: Arc>, - beta_participants: Arc>>, - utxo_addresses: Arc>>, - encryption_sk: Option, -} - -#[derive(Clone, Default)] -struct BetaTracking { - forwarded_payments: ForwardedPayments, - processed_spends: u64, - total_accumulated_utxo: u64, - total_on_track_utxo: u64, - total_royalties: BTreeMap, -} - -/// Map of Discord usernames to their tracked forwarded payments -type ForwardedPayments = BTreeMap>; - -type UtxoStatus = (u64, Instant, NanoTokens); - -type PartitionedUtxoStatus = ( - BTreeMap, - BTreeMap, -); - -#[derive(Clone, Serialize, Deserialize)] -struct SpendJsonResponse { - address: String, - fault: String, - spend_type: String, - spends: Vec, -} - -impl SpendDagDb { - /// Create a new SpendDagDb - /// If a local spend DAG file is found, it will be loaded - /// Else a new DAG will be created containing only Genesis - pub async fn new( - path: PathBuf, - client: Client, - encryption_sk: Option, - ) -> Result { - if !path.exists() { - debug!("Creating directory {path:?}..."); - std::fs::create_dir_all(&path)?; - } - let dag_path = path.join(SPEND_DAG_FILENAME); - info!("Loading DAG from {dag_path:?}..."); - let dag = match SpendDag::load_from_file(&dag_path) { - Ok(d) => { - info!("Found a local spend DAG file"); - d - } - Err(_) => { - info!("Found no local spend DAG file, starting from Genesis"); - client.new_dag_with_genesis_only().await? - } - }; - - Ok(Self { - client: Some(client), - path, - dag: Arc::new(RwLock::new(dag)), - beta_tracking: Arc::new(RwLock::new(Default::default())), - beta_participants: Arc::new(RwLock::new(BTreeMap::new())), - utxo_addresses: Arc::new(RwLock::new(BTreeMap::new())), - encryption_sk, - }) - } - - // Check if the DAG has an encryption secret key set - pub fn has_encryption_sk(&self) -> bool { - self.encryption_sk.is_some() - } - - /// Create a new SpendDagDb from a local file and no network connection - pub fn offline(dag_path: PathBuf, encryption_sk: Option) -> Result { - let path = dag_path - .parent() - .ok_or_else(|| eyre!("Failed to get parent path"))? - .to_path_buf(); - let dag = SpendDag::load_from_file(&dag_path)?; - Ok(Self { - client: None, - path, - dag: Arc::new(RwLock::new(dag)), - beta_tracking: Arc::new(RwLock::new(Default::default())), - beta_participants: Arc::new(RwLock::new(BTreeMap::new())), - utxo_addresses: Arc::new(RwLock::new(BTreeMap::new())), - encryption_sk, - }) - } - - /// Get info about a single spend in JSON format - pub async fn spend_json(&self, address: SpendAddress) -> Result { - let dag_ref = Arc::clone(&self.dag); - let r_handle = dag_ref.read().await; - let spend = r_handle.get_spend(&address); - let faults = r_handle.get_spend_faults(&address); - let fault = if faults.is_empty() { - "none".to_string() - } else { - faults.iter().fold(String::new(), |mut output, b| { - let _ = write!(output, "{b:?}; "); - output - }) - }; - - let (spend_type, spends) = match spend { - SpendDagGet::SpendNotFound => ("SpendNotFound", vec![]), - SpendDagGet::Utxo => ("Utxo", vec![]), - SpendDagGet::DoubleSpend(vs) => ("DoubleSpend", vs), - SpendDagGet::Spend(s) => ("Spend", vec![*s]), - }; - - let spend_json = SpendJsonResponse { - address: address.to_hex(), - fault, - spend_type: spend_type.to_string(), - spends, - }; - - let json = serde_json::to_string_pretty(&spend_json)?; - Ok(json) - } - - /// Dump DAG to disk - pub async fn dump(&self) -> Result<()> { - std::fs::create_dir_all(&self.path)?; - let dag_path = self.path.join(SPEND_DAG_FILENAME); - let dag_ref = Arc::clone(&self.dag); - let r_handle = dag_ref.read().await; - r_handle.dump_to_file(dag_path)?; - Ok(()) - } - - /// Load current DAG svg from disk - #[cfg(feature = "svg-dag")] - pub fn load_svg(&self) -> Result> { - let svg_path = self.path.join(SPEND_DAG_SVG_FILENAME); - let svg = std::fs::read(&svg_path) - .context(format!("Could not load svg from path: {svg_path:?}"))?; - Ok(svg) - } - - /// Dump current DAG as svg to disk - #[cfg(feature = "svg-dag")] - pub async fn dump_dag_svg(&self) -> Result<()> { - info!("Dumping DAG to svg..."); - std::fs::create_dir_all(&self.path)?; - let svg_path = self.path.join(SPEND_DAG_SVG_FILENAME); - let dag_ref = Arc::clone(&self.dag); - let r_handle = dag_ref.read().await; - let svg = dag_to_svg(&r_handle)?; - std::fs::write(svg_path.clone(), svg)?; - info!("Successfully dumped DAG to {svg_path:?}..."); - Ok(()) - } - - /// Update DAG from Network continuously - pub async fn continuous_background_update(self, storage_dir: PathBuf) -> Result<()> { - let client = if let Some(client) = &self.client { - client.clone() - } else { - bail!("Cannot update DAG in offline mode") - }; - - // init utxos to fetch - let start_dag = { Arc::clone(&self.dag).read().await.clone() }; - { - let mut utxo_addresses = self.utxo_addresses.write().await; - for addr in start_dag.get_utxos().iter() { - info!("Tracking genesis UTXO {addr:?}"); - // The UTXO holding 30% will never be used, hence be counted as 0 - let _ = utxo_addresses.insert(*addr, (0, Instant::now(), NanoTokens::zero())); - } - } - - // beta rewards processing - let self_clone = self.clone(); - let spend_processing = if let Some(sk) = self.encryption_sk.clone() { - let (tx, mut rx) = tokio::sync::mpsc::channel::<(SignedSpend, u64, bool)>( - SPENDS_PROCESSING_BUFFER_SIZE, - ); - tokio::spawn(async move { - let mut double_spends = BTreeSet::new(); - let mut detected_spends = BTreeSet::new(); - - while let Some((spend, utxos_for_further_track, is_double_spend)) = rx.recv().await - { - let content_hash = spend.spend.hash(); - - if detected_spends.insert(content_hash) { - let hex_content_hash = content_hash.to_hex(); - let addr_hex = spend.address().to_hex(); - let file_name = format!("{addr_hex}_{hex_content_hash}"); - let spend_copy = spend.clone(); - let file_path = storage_dir.join(&file_name); - - tokio::spawn(async move { - let bytes = spend_copy.to_bytes(); - match std::fs::write(&file_path, bytes) { - Ok(_) => { - info!("Wrote spend {file_name} to disk!"); - } - Err(err) => { - error!("Error writing spend {file_name}, error: {err:?}"); - } - } - }); - } - - if is_double_spend { - self_clone - .beta_background_process_double_spend( - spend.clone(), - &sk, - utxos_for_further_track, - ) - .await; - - // For double_spend, only credit the owner first time - // The performance track only count the received spend & utxos once. - if double_spends.insert(spend.address()) { - self_clone - .beta_background_process_spend(spend, &sk, utxos_for_further_track) - .await; - } - } else { - self_clone - .beta_background_process_spend(spend, &sk, utxos_for_further_track) - .await; - } - } - }); - Some(tx) - } else { - warn!("Foundation secret key not set! Beta rewards will not be processed."); - None - }; - - let mut addrs_to_get = BTreeMap::new(); - let mut addrs_fetched = BTreeSet::new(); - - loop { - // get expired utxos for re-attempt fetch - { - let now = Instant::now(); - let utxo_addresses = self.utxo_addresses.read().await; - for (address, (failure_times, time_stamp, amount)) in utxo_addresses.iter() { - if now > *time_stamp { - if amount.as_nano() > 100000 { - info!("re-attempt fetching big-UTXO {address:?} with {amount}"); - } - let _ = addrs_to_get.insert(*address, (*failure_times, *amount)); - } - } - } - - if addrs_to_get.is_empty() { - debug!( - "Sleeping for {:?} until next re-attempt...", - *DAG_CRAWL_REST_INTERVAL - ); - tokio::time::sleep(*DAG_CRAWL_REST_INTERVAL).await; - continue; - } - - if cfg!(feature = "dag-collection") { - let new_utxos = self - .crawl_and_generate_local_dag( - addrs_to_get.keys().copied().collect(), - spend_processing.clone(), - client.clone(), - ) - .await; - addrs_to_get.clear(); - - let mut utxo_addresses = self.utxo_addresses.write().await; - utxo_addresses.extend(new_utxos.into_iter().map(|a| { - ( - a, - ( - 0, - Instant::now() + *UTXO_REATTEMPT_INTERVAL, - NanoTokens::zero(), - ), - ) - })); - } else if let Some(sender) = spend_processing.clone() { - let (reattempt_addrs, fetched_addrs, addrs_for_further_track) = client - .crawl_to_next_utxos( - addrs_to_get.clone(), - sender.clone(), - *UTXO_REATTEMPT_SECONDS, - ) - .await; - - addrs_to_get.clear(); - let mut utxo_addresses = self.utxo_addresses.write().await; - for addr in fetched_addrs { - let _ = utxo_addresses.remove(&addr); - let _ = addrs_fetched.insert(addr); - } - for (addr, tuple) in reattempt_addrs { - let _ = utxo_addresses.insert(addr, tuple); - } - for (addr, amount) in addrs_for_further_track { - if !addrs_fetched.contains(&addr) { - let _ = addrs_to_get.entry(addr).or_insert((0, amount)); - } - } - } else { - panic!("There is no point in running the auditor if we are not collecting the DAG or collecting data through crawling. Please enable the `dag-collection` feature or provide beta program related arguments."); - }; - } - } - - async fn crawl_and_generate_local_dag( - &self, - from: BTreeSet, - spend_processing: Option>, - client: Client, - ) -> BTreeSet { - // get a copy of the current DAG - let mut dag = { Arc::clone(&self.dag).read().await.clone() }; - - // update it - client - .spend_dag_continue_from(&mut dag, from, spend_processing.clone(), true) - .await; - let new_utxos = dag.get_utxos(); - - // write updates to local DAG and save to disk - let mut dag_w_handle = self.dag.write().await; - *dag_w_handle = dag; - std::mem::drop(dag_w_handle); - if let Err(e) = self.dump().await { - error!("Failed to dump DAG: {e}"); - } - - // update and save svg to file in a background thread so we don't block - #[cfg(feature = "svg-dag")] - { - let self_clone = self.clone(); - tokio::spawn(async move { - if let Err(e) = self_clone.dump_dag_svg().await { - error!("Failed to dump DAG svg: {e}"); - } - }); - } - - new_utxos - } - - /// Process each spend and update beta rewards data - pub async fn beta_background_process_spend( - &self, - spend: SignedSpend, - sk: &SecretKey, - utxos_for_further_track: u64, - ) { - let mut beta_tracking = self.beta_tracking.write().await; - beta_tracking.processed_spends += 1; - beta_tracking.total_accumulated_utxo += spend.spend.descendants.len() as u64; - beta_tracking.total_on_track_utxo += utxos_for_further_track; - - // Collect royalties - let royalty_pubkeys: BTreeSet<_> = spend - .spend - .network_royalties() - .iter() - .map(|(_, _, derivation_idx)| NETWORK_ROYALTIES_PK.new_unique_pubkey(derivation_idx)) - .collect(); - let default_royalty_pubkeys: BTreeSet<_> = spend - .spend - .network_royalties() - .iter() - .map(|(_, _, derivation_idx)| { - DEFAULT_NETWORK_ROYALTIES_PK.new_unique_pubkey(derivation_idx) - }) - .collect(); - let mut royalties = BTreeMap::new(); - for (unique_pk, amount) in spend.spend.descendants.iter() { - if default_royalty_pubkeys.contains(unique_pk) || royalty_pubkeys.contains(unique_pk) { - let _ = royalties.insert( - SpendAddress::from_unique_pubkey(unique_pk), - amount.as_nano(), - ); - } - } - - if royalties.len() > (spend.spend.descendants.len() - 1) / 2 { - eprintln!( - "Spend: {:?} has incorrect royalty of {}, with amount {} with reason {:?}", - spend.spend.unique_pubkey, - royalties.len(), - spend.spend.amount().as_nano(), - spend.spend.reason - ); - eprintln!( - "Incorrect royalty spend has {} royalties, {:?} - {:?}", - spend.spend.network_royalties().len(), - spend.spend.ancestors, - spend.spend.descendants - ); - warn!( - "Spend: {:?} has incorrect royalty of {}, with amount {} with reason {:?}", - spend.spend.unique_pubkey, - royalties.len(), - spend.spend.amount().as_nano(), - spend.spend.reason - ); - warn!( - "Incorrect royalty spend has {} royalties, {:?} - {:?}", - spend.spend.network_royalties().len(), - spend.spend.ancestors, - spend.spend.descendants - ); - } - beta_tracking.total_royalties.extend(royalties); - - let addr = spend.address(); - let amount = spend.spend.amount(); - - // check for beta rewards reason - let user_name_hash = match spend.reason().decrypt_discord_cypher(sk) { - Some(n) => n, - None => { - if let Some(default_user_name_hash) = spend - .reason() - .decrypt_discord_cypher(&DEFAULT_PAYMENT_FORWARD_SK) - { - warn!("With default key, got forwarded reward of {amount} at {addr:?}"); - println!("With default key, got forwarded reward of {amount} at {addr:?}"); - default_user_name_hash - } else { - info!("Spend {addr:?} is not for reward forward."); - println!("Spend {addr:?} is not for reward forward."); - return; - } - } - }; - - // add to local rewards - let addr = spend.address(); - let amount = spend.spend.amount(); - let beta_participants_read = self.beta_participants.read().await; - - if let Some(user_name) = beta_participants_read.get(&user_name_hash) { - trace!("Got forwarded reward {amount} from {user_name} of {amount} at {addr:?}"); - beta_tracking - .forwarded_payments - .entry(user_name.to_owned()) - .or_default() - .insert((addr, amount)); - } else { - // check with default key - if let Some(default_user_name_hash) = spend - .reason() - .decrypt_discord_cypher(&DEFAULT_PAYMENT_FORWARD_SK) - { - if let Some(user_name) = beta_participants_read.get(&default_user_name_hash) { - warn!("With default key, got forwarded reward from {user_name} of {amount} at {addr:?}"); - println!("With default key, got forwarded reward from {user_name} of {amount} at {addr:?}"); - beta_tracking - .forwarded_payments - .entry(user_name.to_owned()) - .or_default() - .insert((addr, amount)); - return; - } - } - - warn!("Found a forwarded reward {amount} for an unknown participant at {addr:?}: {user_name_hash:?}"); - beta_tracking - .forwarded_payments - .entry(format!("unknown participant: {user_name_hash:?}")) - .or_default() - .insert((addr, amount)); - } - } - - async fn beta_background_process_double_spend( - &self, - spend: SignedSpend, - sk: &SecretKey, - _utxos_for_further_track: u64, - ) { - let user_name_hash = match spend.reason().decrypt_discord_cypher(sk) { - Some(n) => n, - None => { - return; - } - }; - - let addr = spend.address(); - - let beta_participants_read = self.beta_participants.read().await; - - if let Some(user_name) = beta_participants_read.get(&user_name_hash) { - println!("Found double spend from {user_name} at {addr:?}"); - } else { - if let Some(default_user_name_hash) = spend - .reason() - .decrypt_discord_cypher(&DEFAULT_PAYMENT_FORWARD_SK) - { - if let Some(user_name) = beta_participants_read.get(&default_user_name_hash) { - println!("Found double spend from {user_name} at {addr:?} using default key"); - return; - } - } - - println!( - "Found double spend from an unknown participant {user_name_hash:?} at {addr:?}" - ); - } - } - - /// Merge a SpendDag into the current DAG - /// This can be used to enrich our DAG with a DAG from another node to avoid costly computations - /// Make sure to verify the other DAG is trustworthy before calling this function to merge it in - pub async fn merge(&mut self, other: SpendDag) -> Result<()> { - let mut w_handle = self.dag.write().await; - w_handle.merge(other, true)?; - Ok(()) - } - - /// Returns the current state of the beta program in JSON format, - /// including total rewards for each participant. - /// Also returns the current tracking performance in readable format. - pub(crate) async fn beta_program_json(&self) -> Result<(String, String)> { - let r_handle = Arc::clone(&self.beta_tracking); - let beta_tracking = r_handle.read().await; - let r_utxo_handler = Arc::clone(&self.utxo_addresses); - let utxo_addresses = r_utxo_handler.read().await; - let mut rewards_output = vec![]; - let mut total_hits = 0_u64; - let mut total_amount = 0_u64; - for (participant, rewards) in beta_tracking.forwarded_payments.iter() { - total_hits += rewards.len() as u64; - let total_rewards = rewards - .iter() - .map(|(_, amount)| amount.as_nano()) - .sum::(); - total_amount += total_rewards; - - rewards_output.push((participant.clone(), total_rewards)); - } - let json = serde_json::to_string_pretty(&rewards_output)?; - - let mut tracking_performance = format!("processed_spends: {}\ntotal_accumulated_utxo:{}\ntotal_on_track_utxo:{}\nskipped_utxo:{}\nrepeated_utxo:{}\ntotal_hits:{}\ntotal_amount:{}", - beta_tracking.processed_spends, beta_tracking.total_accumulated_utxo, beta_tracking.total_on_track_utxo, beta_tracking.total_accumulated_utxo - beta_tracking.total_on_track_utxo, - utxo_addresses.len(), total_hits, total_amount - ); - - tracking_performance = format!( - "{tracking_performance}\ntotal_royalties hits: {}", - beta_tracking.total_royalties.len() - ); - let total_royalties = beta_tracking.total_royalties.values().sum::(); - tracking_performance = - format!("{tracking_performance}\ntotal_royalties amount: {total_royalties}"); - - // UTXO amount that greater than 100000 nanos shall be considered as `change` - // which indicates the `wallet balance` - let (big_utxos, small_utxos): PartitionedUtxoStatus = - utxo_addresses - .iter() - .partition(|(_address, (_failure_times, _time_stamp, amount))| { - amount.as_nano() > 100000 - }); - - let total_big_utxo_amount = big_utxos - .iter() - .map(|(_addr, (_failure_times, _time, amount))| amount.as_nano()) - .sum::(); - tracking_performance = - format!("{tracking_performance}\ntotal_big_utxo_amount: {total_big_utxo_amount}"); - - let total_small_utxo_amount = small_utxos - .iter() - .map(|(_addr, (_failure_times, _time, amount))| amount.as_nano()) - .sum::(); - tracking_performance = - format!("{tracking_performance}\ntotal_small_utxo_amount: {total_small_utxo_amount}"); - - for (addr, (_failure_times, _time, amount)) in big_utxos.iter() { - tracking_performance = - format!("{tracking_performance}\n{addr:?}, {}", amount.as_nano()); - } - for (addr, (_failure_times, _time, amount)) in small_utxos.iter() { - tracking_performance = - format!("{tracking_performance}\n{addr:?}, {}", amount.as_nano()); - } - - Ok((json, tracking_performance)) - } - - /// Track new beta participants. This just add the participants to the list of tracked participants. - pub(crate) async fn track_new_beta_participants( - &self, - participants: BTreeSet, - ) -> Result<()> { - let mut new_participants = vec![]; - // track new participants - { - let mut beta_participants = self.beta_participants.write().await; - beta_participants.extend(participants.iter().map(|p| { - let hash: Hash = Hash::hash(p.as_bytes()); - new_participants.push((hash, p.clone())); - (hash, p.clone()) - })); - } - // initialize forwarded payments - { - let mut beta_tracking = self.beta_tracking.write().await; - for (hash, p) in new_participants { - let unkown_str = format!("unknown participant: {hash:?}"); - let mut payments = beta_tracking - .forwarded_payments - .remove(&unkown_str) - .unwrap_or_default(); - - if let Some(existing) = beta_tracking - .forwarded_payments - .insert(p.clone(), payments.clone()) - { - warn!("Overwriting existing participant {p} with new participant {hash:?}"); - payments.extend(existing); - let _ = beta_tracking.forwarded_payments.insert(p.clone(), payments); - } - } - } - Ok(()) - } - - /// Check if a participant is being tracked - pub(crate) async fn is_participant_tracked(&self, discord_id: &str) -> Result { - let beta_participants = self.beta_participants.read().await; - debug!("Existing beta participants: {beta_participants:?}"); - - debug!( - "Adding new beta participants: {discord_id}, {:?}", - Hash::hash(discord_id.as_bytes()) - ); - Ok(beta_participants.contains_key(&Hash::hash(discord_id.as_bytes()))) - } - - /// Backup beta rewards to a timestamped json file - pub(crate) async fn backup_rewards(&self) -> Result<()> { - info!("Beta rewards backup requested"); - let (json, tracking_performance) = match self.beta_program_json().await { - Ok(r) => r, - Err(e) => bail!("Failed to get beta rewards json: {e}"), - }; - - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|t| format!("{t:?}")) - .unwrap_or_default(); - let backup_file = self.path.join(format!("beta_rewards_{timestamp}.json")); - info!("Writing rewards backup to {backup_file:?}"); - std::fs::write(backup_file, json) - .map_err(|e| eyre!("Could not write rewards backup to disk: {e}"))?; - - let backup_file = self - .path - .join(format!("tracking_performance_{timestamp}.log")); - info!("Writing tracking performance to {backup_file:?}"); - std::fs::write(backup_file, tracking_performance) - .map_err(|e| eyre!("Could not write tracking performance to disk: {e}"))?; - - Ok(()) - } -} - -#[cfg(feature = "svg-dag")] -fn dag_to_svg(dag: &SpendDag) -> Result> { - let dot = dag.dump_dot_format(); - let graph = parse(&dot).map_err(|err| eyre!("Failed to parse dag from dot: {err}"))?; - let graph_svg = exec( - graph, - &mut PrinterContext::default(), - vec![Format::Svg.into()], - ) - .map_err(|e| eyre!("Failed to generate svg, is graphviz installed? dot: {e}"))?; - let svg = quick_edit_svg(graph_svg, dag)?; - Ok(svg) -} - -// quick n dirty svg editing -// - makes spends clickable -// - spend address reveals on hover -// - marks poisoned spends as red -// - marks UTXOs and unknown ancestors as yellow -// - just pray it works on windows -#[cfg(feature = "svg-dag")] -fn quick_edit_svg(svg: Vec, dag: &SpendDag) -> Result> { - let mut str = String::from_utf8(svg).map_err(|err| eyre!("Failed svg conversion: {err}"))?; - - let spend_addrs: Vec<_> = dag.all_spends().iter().map(|s| s.address()).collect(); - let pending_addrs = dag.get_pending_spends(); - let all_addrs = spend_addrs.iter().chain(pending_addrs.iter()); - - for addr in all_addrs { - let addr_hex = addr.to_hex().to_string(); - let is_fault = !dag.get_spend_faults(addr).is_empty(); - let is_known_but_not_gathered = matches!(dag.get_spend(addr), SpendDagGet::Utxo); - let colour = if is_fault { - "red" - } else if is_known_but_not_gathered { - "yellow" - } else { - "none" - }; - - let link = format!(""); - let idxs = dag.get_spend_indexes(addr); - for i in idxs { - let title = format!("{i}\n{addr_hex}\n{link}\n\n"); - let new_end = format!("{addr:?}\n\n"); - str = str.replace(&end, &new_end); - } - - Ok(str.into_bytes()) -} diff --git a/sn_auditor/src/main.rs b/sn_auditor/src/main.rs deleted file mode 100644 index 8a340d55fe..0000000000 --- a/sn_auditor/src/main.rs +++ /dev/null @@ -1,416 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -#[macro_use] -extern crate tracing; - -mod dag_db; -mod routes; - -use bls::SecretKey; -use clap::Parser; -use color_eyre::eyre::{eyre, Result}; -use dag_db::SpendDagDb; -use sn_client::Client; -use sn_logging::{Level, LogBuilder, LogFormat, LogOutputDest}; -use sn_peers_acquisition::PeersArgs; -use sn_protocol::version::IDENTIFY_PROTOCOL_STR; -use std::collections::BTreeSet; -use std::path::PathBuf; -use tiny_http::{Response, Server}; - -/// Backup the beta rewards in a timestamped json file -const BETA_REWARDS_BACKUP_INTERVAL_SECS: u64 = 20 * 60; - -#[derive(Parser)] -#[command(disable_version_flag = true)] -struct Opt { - #[command(flatten)] - peers: PeersArgs, - /// Force the spend DAG to be updated from genesis - #[clap(short, long)] - force_from_genesis: bool, - /// Clear the local spend DAG and start from scratch - #[clap(short, long)] - clean: bool, - /// Visualize a local DAG file offline, does not connect to the Network - #[clap(short, long, value_name = "dag_file")] - offline_viewer: Option, - - /// Specify the logging output destination. - /// - /// Valid values are "stdout", "data-dir", or a custom path. - /// - /// `data-dir` is the default value. - /// - /// The data directory location is platform specific: - /// - Linux: $HOME/.local/share/safe/client/logs - /// - macOS: $HOME/Library/Application Support/safe/client/logs - /// - Windows: C:\Users\\AppData\Roaming\safe\client\logs - #[clap(long, value_parser = LogOutputDest::parse_from_str, verbatim_doc_comment, default_value = "data-dir")] - log_output_dest: LogOutputDest, - /// Specify the logging format. - /// - /// Valid values are "default" or "json". - /// - /// If the argument is not used, the default format will be applied. - #[clap(long, value_parser = LogFormat::parse_from_str, verbatim_doc_comment)] - log_format: Option, - - /// Beta rewards program participants to track - /// Provide a JSON file with a list of Discord usernames as argument - #[clap(short, long, value_name = "discord_names_file")] - beta_participants: Option, - - /// Secret encryption key of the beta rewards to decypher - /// discord usernames of the beta participants - #[clap(short = 'k', long, value_name = "hex_secret_key")] - beta_encryption_key: Option, - - /// Print the crate version. - #[clap(long)] - pub crate_version: bool, - - /// Print the network protocol version. - #[clap(long)] - pub protocol_version: bool, - - /// Print the package version. - #[cfg(not(feature = "nightly"))] - #[clap(long)] - pub package_version: bool, - - /// Print version information. - #[clap(long)] - version: bool, -} - -#[tokio::main] -async fn main() -> Result<()> { - let opt = Opt::parse(); - - if opt.version { - println!( - "{}", - sn_build_info::version_string( - "Autonomi Auditor", - env!("CARGO_PKG_VERSION"), - Some(&IDENTIFY_PROTOCOL_STR) - ) - ); - return Ok(()); - } - - if opt.crate_version { - println!("{}", env!("CARGO_PKG_VERSION")); - return Ok(()); - } - - #[cfg(not(feature = "nightly"))] - if opt.package_version { - println!("{}", sn_build_info::package_version()); - return Ok(()); - } - - if opt.protocol_version { - println!("{}", *IDENTIFY_PROTOCOL_STR); - return Ok(()); - } - - let log_builder = logging_init(opt.log_output_dest, opt.log_format)?; - let _log_handles = log_builder.initialize()?; - let beta_participants = load_and_update_beta_participants(opt.beta_participants)?; - - let maybe_sk = if let Some(sk_str) = opt.beta_encryption_key { - match SecretKey::from_hex(&sk_str) { - Ok(sk) => Some(sk), - Err(err) => panic!("Cann't parse Foundation SK from input string: {sk_str}: {err:?}",), - } - } else { - None - }; - let beta_rewards_on = maybe_sk.is_some(); - - if let Some(dag_to_view) = opt.offline_viewer { - let dag = SpendDagDb::offline(dag_to_view, maybe_sk)?; - #[cfg(feature = "svg-dag")] - dag.dump_dag_svg().await?; - - start_server(dag).await?; - return Ok(()); - } - - let client = connect_to_network(opt.peers).await?; - - let storage_dir = get_auditor_data_dir_path()?.join("fetched_spends"); - std::fs::create_dir_all(&storage_dir).expect("fetched_spends path to be successfully created."); - - let dag = initialize_background_spend_dag_collection( - client.clone(), - opt.force_from_genesis, - opt.clean, - beta_participants, - maybe_sk, - storage_dir, - ) - .await?; - - if beta_rewards_on { - initialize_background_rewards_backup(dag.clone()); - } - - start_server(dag).await -} - -fn logging_init( - log_output_dest: LogOutputDest, - log_format: Option, -) -> Result { - color_eyre::install()?; - let logging_targets = vec![ - ("sn_auditor".to_string(), Level::TRACE), - ("sn_client".to_string(), Level::DEBUG), - ("sn_transfers".to_string(), Level::TRACE), - ("sn_logging".to_string(), Level::INFO), - ("sn_peers_acquisition".to_string(), Level::INFO), - ("sn_protocol".to_string(), Level::INFO), - ("sn_networking".to_string(), Level::WARN), - ]; - let mut log_builder = LogBuilder::new(logging_targets); - log_builder.output_dest(log_output_dest); - log_builder.format(log_format.unwrap_or(LogFormat::Default)); - Ok(log_builder) -} - -async fn connect_to_network(peers_args: PeersArgs) -> Result { - let bootstrap_peers = peers_args.get_peers().await?; - info!( - "Connecting to the network with {} bootstrap peers", - bootstrap_peers.len(), - ); - let bootstrap_peers = if bootstrap_peers.is_empty() { - // empty vec is returned if `local` flag is provided - None - } else { - Some(bootstrap_peers) - }; - let client = Client::new(SecretKey::random(), bootstrap_peers, None, None) - .await - .map_err(|err| eyre!("Failed to connect to the network: {err}"))?; - - info!("Connected to the network"); - Ok(client) -} - -/// Regularly backup the rewards in a timestamped json file -fn initialize_background_rewards_backup(dag: SpendDagDb) { - tokio::spawn(async move { - loop { - trace!( - "Sleeping for {BETA_REWARDS_BACKUP_INTERVAL_SECS} seconds before next backup..." - ); - tokio::time::sleep(tokio::time::Duration::from_secs( - BETA_REWARDS_BACKUP_INTERVAL_SECS, - )) - .await; - info!("Backing up beta rewards..."); - - if let Err(e) = dag.backup_rewards().await { - error!("Failed to backup beta rewards: {e}"); - } - } - }); -} - -/// Get DAG from disk or initialize it if it doesn't exist -/// Spawn a background thread to update the DAG in the background -/// Return a handle to the DAG -async fn initialize_background_spend_dag_collection( - client: Client, - force_from_genesis: bool, - clean: bool, - beta_participants: BTreeSet, - foundation_sk: Option, - storage_dir: PathBuf, -) -> Result { - info!("Initialize spend dag..."); - let path = get_auditor_data_dir_path()?; - if !path.exists() { - debug!("Creating directory {path:?}..."); - std::fs::create_dir_all(&path)?; - } - - // clean the local spend DAG if requested - if clean { - info!("Cleaning local spend DAG..."); - let dag_file = path.join(dag_db::SPEND_DAG_FILENAME); - let _ = std::fs::remove_file(dag_file).map_err(|e| error!("Cleanup interrupted: {e}")); - } - - // initialize the DAG - let dag = dag_db::SpendDagDb::new(path.clone(), client.clone(), foundation_sk) - .await - .map_err(|e| eyre!("Could not create SpendDag Db: {e}"))?; - - // optional force restart from genesis and merge into our current DAG - // feature guard to prevent a mis-use of opt - if force_from_genesis && cfg!(feature = "dag-collection") { - warn!("Forcing DAG to be updated from genesis..."); - let mut d = dag.clone(); - let mut genesis_dag = client - .new_dag_with_genesis_only() - .await - .map_err(|e| eyre!("Could not create new DAG from genesis: {e}"))?; - tokio::spawn(async move { - client - .spend_dag_continue_from_utxos(&mut genesis_dag, None, true) - .await; - let _ = d - .merge(genesis_dag) - .await - .map_err(|e| error!("Failed to merge from genesis DAG into our DAG: {e}")); - }); - } - - // initialize svg - #[cfg(feature = "svg-dag")] - dag.dump_dag_svg().await?; - - // initialize beta rewards program tracking - if !beta_participants.is_empty() { - if !dag.has_encryption_sk() { - panic!("Foundation SK required to initialize beta rewards program"); - }; - - info!("Initializing beta rewards program tracking..."); - if let Err(e) = dag.track_new_beta_participants(beta_participants).await { - error!("Could not initialize beta rewards: {e}"); - return Err(e); - } - } - - // background thread to update DAG - info!("Starting background DAG collection thread..."); - let d = dag.clone(); - tokio::spawn(async move { - let _ = d - .continuous_background_update(storage_dir) - .await - .map_err(|e| error!("Failed to update DAG in background thread: {e}")); - }); - - Ok(dag) -} - -async fn start_server(dag: SpendDagDb) -> Result<()> { - loop { - let server = Server::http("0.0.0.0:4242").expect("Failed to start server"); - info!("Starting dag-query server listening on port 4242..."); - for request in server.incoming_requests() { - info!( - "Received request! method: {:?}, url: {:?}", - request.method(), - request.url(), - ); - - // Dispatch the request to the appropriate handler - let response = match request.url() { - "/" => routes::spend_dag_svg(&dag), - s if s.starts_with("/spend/") => routes::spend(&dag, &request).await, - s if s.starts_with("/add-participant/") => { - routes::add_participant(&dag, &request).await - } - "/beta-rewards" => routes::beta_rewards(&dag).await, - "/restart" => { - info!("Restart auditor web service as to client's request"); - break; - } - "/terminate" => { - info!("Terminate auditor web service as to client's request"); - return Ok(()); - } - _ => routes::not_found(), - }; - - // Send a response to the client - match response { - Ok(res) => { - info!("Sending response to client"); - let _ = request.respond(res).map_err(|err| { - warn!("Failed to send response: {err}"); - eprintln!("Failed to send response: {err}") - }); - } - Err(e) => { - eprint!("Sending error to client: {e}"); - let res = Response::from_string(format!("Error: {e}")).with_status_code(500); - let _ = request.respond(res).map_err(|err| { - warn!("Failed to send error response: {err}"); - eprintln!("Failed to send error response: {err}") - }); - } - } - } - // Reaching this point indicates a restarting of auditor web service - // Sleep for a while to allowing OS cleanup and settlement. - drop(server); - std::thread::sleep(std::time::Duration::from_secs(10)); - } -} - -// get the data dir path for auditor -fn get_auditor_data_dir_path() -> Result { - let path = dirs_next::data_dir() - .ok_or(eyre!("Could not obtain data directory path"))? - .join("safe") - .join("auditor"); - - Ok(path) -} - -fn load_and_update_beta_participants( - provided_participants_file: Option, -) -> Result> { - let mut beta_participants = if let Some(participants_file) = provided_participants_file { - let raw_data = std::fs::read_to_string(&participants_file)?; - // instead of serde_json, just use a line separated file - let discord_names = raw_data - .lines() - .map(|line| line.trim().to_string()) - .collect::>(); - debug!( - "Tracking beta rewards for the {} discord usernames provided in {:?}", - discord_names.len(), - participants_file - ); - discord_names - } else { - vec![] - }; - // restore beta participants from local cached copy - let local_participants_file = - get_auditor_data_dir_path()?.join(dag_db::BETA_PARTICIPANTS_FILENAME); - if local_participants_file.exists() { - let raw_data = std::fs::read_to_string(&local_participants_file)?; - let discord_names = raw_data - .lines() - .map(|line| line.trim().to_string()) - .collect::>(); - debug!( - "Restoring beta rewards for the {} discord usernames from {:?}", - discord_names.len(), - local_participants_file - ); - beta_participants.extend(discord_names); - } - // write the beta participants to disk - let _ = std::fs::write(local_participants_file, beta_participants.join("\n")) - .map_err(|e| error!("Failed to write beta participants to disk: {e}")); - - Ok(beta_participants.into_iter().collect()) -} diff --git a/sn_auditor/src/routes.rs b/sn_auditor/src/routes.rs deleted file mode 100644 index 8f51a30923..0000000000 --- a/sn_auditor/src/routes.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::dag_db::{self, SpendDagDb}; -use color_eyre::eyre::{eyre, Result}; -use sn_client::transfers::SpendAddress; -use std::{ - collections::BTreeSet, - fs::{File, OpenOptions}, - io::{Cursor, Write}, - str::FromStr, -}; -use tiny_http::{Request, Response}; - -pub(crate) fn spend_dag_svg(_dag: &SpendDagDb) -> Result>>> { - #[cfg(not(feature = "svg-dag"))] - return Ok(Response::from_string( - "SVG DAG not enabled on this server (the host should enable it with the 'svg-dag' feature flag)", - ) - .with_status_code(200)); - - #[cfg(feature = "svg-dag")] - { - let svg = _dag - .load_svg() - .map_err(|e| eyre!("Failed to get SVG: {e}"))?; - let response = Response::from_data(svg); - Ok(response) - } -} - -pub(crate) async fn spend( - dag: &SpendDagDb, - request: &Request, -) -> Result>>> { - let addr = match request.url().split('/').last() { - Some(addr) => addr, - None => { - return Ok(Response::from_string( - "No address provided. Should be /spend/[your_spend_address_here]", - ) - .with_status_code(400)) - } - }; - let spend_addr = match SpendAddress::from_str(addr) { - Ok(addr) => addr, - Err(e) => { - return Ok(Response::from_string(format!( - "Failed to parse address: {e}. Should be /spend/[your_spend_address_here]" - )) - .with_status_code(400)) - } - }; - let json = dag - .spend_json(spend_addr) - .await - .map_err(|e| eyre!("Failed to get spend JSON: {e}"))?; - let response = Response::from_data(json); - Ok(response) -} - -pub(crate) fn not_found() -> Result>>> { - let response = Response::from_string("404: Try /").with_status_code(404); - Ok(response) -} - -pub(crate) async fn beta_rewards(dag: &SpendDagDb) -> Result>>> { - let (json, _) = dag - .beta_program_json() - .await - .map_err(|e| eyre!("Failed to get beta rewards JSON: {e}"))?; - let response = Response::from_data(json); - Ok(response) -} - -pub(crate) async fn add_participant( - dag: &SpendDagDb, - request: &Request, -) -> Result>>> { - let discord_id = match request.url().split('/').last() { - Some(discord_id) => { - // TODO: When we simply accept POST we can remove this decoding - // For now we need it to decode #fragments in urls - let discord_id = urlencoding::decode(discord_id)?; - discord_id.to_string() - } - None => { - return Ok(Response::from_string( - "No discord_id provided. Should be /add-participant/[your_discord_id_here]", - ) - .with_status_code(400)) - } - }; - - if discord_id.chars().count() >= 32 { - return Ok( - Response::from_string("discord_id cannot be more than 32 chars").with_status_code(400), - ); - } else if discord_id.chars().count() == 0 { - return Ok(Response::from_string("discord_id cannot be empty").with_status_code(400)); - } - - if let Err(err) = track_new_participant(dag, discord_id.to_owned()).await { - return Ok( - Response::from_string(format!("Failed to track new participant: {err}")) - .with_status_code(400), - ); - } - - Ok(Response::from_string("Successfully added participant ")) -} - -async fn track_new_participant(dag: &SpendDagDb, discord_id: String) -> Result<()> { - // only append new ids - if dag.is_participant_tracked(&discord_id).await? { - return Ok(()); - } - - dag.track_new_beta_participants(BTreeSet::from_iter([discord_id.to_owned()])) - .await?; - - let local_participants_file = dag.path.join(dag_db::BETA_PARTICIPANTS_FILENAME); - - if local_participants_file.exists() { - let mut file = OpenOptions::new() - .append(true) - .open(local_participants_file) - .map_err(|e| eyre!("Failed to open file: {e}"))?; - writeln!(file, "{discord_id}")?; - } else { - let mut file = File::create(local_participants_file) - .map_err(|e| eyre!("Failed to create file: {e}"))?; - writeln!(file, "{discord_id}")?; - } - - Ok(()) -} diff --git a/sn_evm/src/lib.rs b/sn_evm/src/lib.rs index 49956db39e..45185101fb 100644 --- a/sn_evm/src/lib.rs +++ b/sn_evm/src/lib.rs @@ -28,7 +28,7 @@ mod amount; mod data_payments; mod error; -pub use data_payments::{PaymentQuote, ProofOfPayment, QuotingMetrics}; +pub use data_payments::{PaymentQuote, ProofOfPayment, QuotingMetrics, QUOTE_EXPIRATION_SECS}; /// Types used in the public API pub use amount::{Amount, AttoTokens}; diff --git a/sn_logging/src/layers.rs b/sn_logging/src/layers.rs index 8b75eb2aae..b345c1dc29 100644 --- a/sn_logging/src/layers.rs +++ b/sn_logging/src/layers.rs @@ -285,7 +285,6 @@ fn get_logging_targets(logging_env_value: &str) -> Result> ("sn_protocol".to_string(), Level::TRACE), ("sn_registers".to_string(), Level::INFO), ("sn_service_management".to_string(), Level::TRACE), - ("sn_transfers".to_string(), Level::TRACE), ]); // Override sn_networking if it was not specified. diff --git a/sn_networking/Cargo.toml b/sn_networking/Cargo.toml index 34cc80e53e..726a52e0b0 100644 --- a/sn_networking/Cargo.toml +++ b/sn_networking/Cargo.toml @@ -57,7 +57,6 @@ self_encryption = "~0.30.0" serde = { version = "1.0.133", features = ["derive", "rc"] } sn_build_info = { path = "../sn_build_info", version = "0.1.19" } sn_protocol = { path = "../sn_protocol", version = "0.17.15" } -sn_transfers = { path = "../sn_transfers", version = "0.20.3" } sn_registers = { path = "../sn_registers", version = "0.4.3" } sn_evm = { path = "../sn_evm", version = "0.1.4" } sysinfo = { version = "0.30.8", default-features = false, optional = true } diff --git a/sn_networking/src/cmd.rs b/sn_networking/src/cmd.rs index a1659afabe..ca34abcb2b 100644 --- a/sn_networking/src/cmd.rs +++ b/sn_networking/src/cmd.rs @@ -646,7 +646,7 @@ impl SwarmDriver { match record_header.kind { RecordKind::Chunk => RecordType::Chunk, RecordKind::Scratchpad => RecordType::Scratchpad, - RecordKind::Spend | RecordKind::Register => { + RecordKind::Transaction | RecordKind::Register => { let content_hash = XorName::from_content(&record.value); RecordType::NonChunk(content_hash) } diff --git a/sn_networking/src/error.rs b/sn_networking/src/error.rs index a3bd64eb05..fb6cf07853 100644 --- a/sn_networking/src/error.rs +++ b/sn_networking/src/error.rs @@ -12,8 +12,8 @@ use libp2p::{ swarm::DialError, PeerId, TransportError, }; +use sn_protocol::storage::{Transaction, TransactionAddress}; use sn_protocol::{messages::Response, storage::RecordKind, NetworkAddress, PrettyPrintRecordKey}; -use sn_transfers::{SignedSpend, SpendAddress}; use std::{ collections::{HashMap, HashSet}, fmt::Debug, @@ -45,7 +45,7 @@ pub enum GetRecordError { RecordNotFound, // Avoid logging the whole `Record` content by accident. /// The split record error will be handled at the network layer. - /// For spends, it accumulates the spends and returns a double spend error if more than one. + /// For transactions, it accumulates the transactions and returns a double transaction error if more than one. /// For registers, it merges the registers and returns the merged record. #[error("Split Record has {} different copies", result_map.len())] SplitRecord { @@ -103,10 +103,6 @@ pub enum NetworkError { #[error("SnProtocol Error: {0}")] ProtocolError(#[from] sn_protocol::error::Error), - #[error("Wallet Error {0}")] - Wallet(#[from] sn_transfers::WalletError), - #[error("Transfer Error {0}")] - Transfer(#[from] sn_transfers::TransferError), #[error("Evm payment Error {0}")] EvmPaymemt(#[from] sn_evm::EvmError), @@ -128,7 +124,7 @@ pub enum NetworkError { InCorrectRecordHeader, // ---------- Transfer Errors - #[error("Failed to get spend: {0}")] + #[error("Failed to get transaction: {0}")] FailedToGetSpend(String), #[error("Transfer is invalid: {0}")] InvalidTransfer(String), @@ -139,9 +135,9 @@ pub enum NetworkError { // ---------- Spend Errors #[error("Spend not found: {0:?}")] - NoSpendFoundInsideRecord(SpendAddress), - #[error("Double spend(s) attempt was detected. The signed spends are: {0:?}")] - DoubleSpendAttempt(Vec), + NoSpendFoundInsideRecord(TransactionAddress), + #[error("Double transaction(s) attempt was detected. The signed transactions are: {0:?}")] + DoubleSpendAttempt(Vec), // ---------- Store Error #[error("No Store Cost Responses")] diff --git a/sn_networking/src/event/kad.rs b/sn_networking/src/event/kad.rs index a2c0a4443c..3eac9f9a6d 100644 --- a/sn_networking/src/event/kad.rs +++ b/sn_networking/src/event/kad.rs @@ -7,7 +7,7 @@ // permissions and limitations relating to use of the SAFE Network Software. use crate::{ - driver::PendingGetClosestType, get_quorum_value, get_raw_signed_spends_from_record, + driver::PendingGetClosestType, get_quorum_value, get_transactions_from_record, target_arch::Instant, GetRecordCfg, GetRecordError, NetworkError, Result, SwarmDriver, CLOSE_GROUP_SIZE, }; @@ -17,10 +17,9 @@ use libp2p::kad::{ QueryStats, Record, K_VALUE, }; use sn_protocol::{ - storage::{try_serialize_record, RecordKind}, + storage::{try_serialize_record, RecordKind, Transaction}, NetworkAddress, PrettyPrintRecordKey, }; -use sn_transfers::SignedSpend; use std::collections::{hash_map::Entry, BTreeSet, HashSet}; use tokio::sync::oneshot; use xor_name::XorName; @@ -397,23 +396,27 @@ impl SwarmDriver { Self::send_record_after_checking_target(senders, peer_record.record, &cfg)?; } else { debug!("For record {pretty_key:?} task {query_id:?}, fetch completed with split record"); - let mut accumulated_spends = BTreeSet::new(); + let mut accumulated_transactions = BTreeSet::new(); for (record, _) in result_map.values() { - match get_raw_signed_spends_from_record(record) { - Ok(spends) => { - accumulated_spends.extend(spends); + match get_transactions_from_record(record) { + Ok(transactions) => { + accumulated_transactions.extend(transactions); } Err(_) => { continue; } } } - if !accumulated_spends.is_empty() { - info!("For record {pretty_key:?} task {query_id:?}, found split record for a spend, accumulated and sending them as a single record"); - let accumulated_spends = - accumulated_spends.into_iter().collect::>(); - - let bytes = try_serialize_record(&accumulated_spends, RecordKind::Spend)?; + if !accumulated_transactions.is_empty() { + info!("For record {pretty_key:?} task {query_id:?}, found split record for a transaction, accumulated and sending them as a single record"); + let accumulated_transactions = accumulated_transactions + .into_iter() + .collect::>(); + + let bytes = try_serialize_record( + &accumulated_transactions, + RecordKind::Transaction, + )?; let new_accumulated_record = Record { key: peer_record.record.key, diff --git a/sn_networking/src/event/request_response.rs b/sn_networking/src/event/request_response.rs index 7dacaa93e4..6ba8c50c31 100644 --- a/sn_networking/src/event/request_response.rs +++ b/sn_networking/src/event/request_response.rs @@ -208,7 +208,7 @@ impl SwarmDriver { // On receive a replication_list from a close_group peer, we undertake: // 1, For those keys that we don't have: // fetch them if close enough to us - // 2, For those spends that we have that differ in the hash, we fetch the other version + // 2, For those transactions that we have that differ in the hash, we fetch the other version // and update our local copy. let all_keys = self .swarm diff --git a/sn_networking/src/lib.rs b/sn_networking/src/lib.rs index c6de3925c3..083f66a4fb 100644 --- a/sn_networking/src/lib.rs +++ b/sn_networking/src/lib.rs @@ -25,9 +25,8 @@ mod record_store; mod record_store_api; mod relay_manager; mod replication_fetcher; -mod spends; pub mod target_arch; -mod transfers; +mod transactions; mod transport; use cmd::LocalSwarmCmd; @@ -42,7 +41,7 @@ pub use self::{ error::{GetRecordError, NetworkError}, event::{MsgResponder, NetworkEvent}, record_store::{calculate_cost_for_records, NodeRecordStore}, - transfers::{get_raw_signed_spends_from_record, get_signed_spend_from_record}, + transactions::get_transactions_from_record, }; #[cfg(feature = "open-metrics")] pub use metrics::service::MetricsRegistries; @@ -76,11 +75,11 @@ use tokio::sync::{ }; use tokio::time::Duration; use { + sn_protocol::storage::Transaction, sn_protocol::storage::{ try_deserialize_record, try_serialize_record, RecordHeader, RecordKind, }, sn_registers::SignedRegister, - sn_transfers::SignedSpend, std::collections::HashSet, }; @@ -514,8 +513,8 @@ impl Network { /// In case a target_record is provided, only return when fetched target. /// Otherwise count it as a failure when all attempts completed. /// - /// It also handles the split record error for spends and registers. - /// For spends, it accumulates the spends and returns an error if more than one. + /// It also handles the split record error for transactions and registers. + /// For transactions, it accumulates the transactions and returns an error if more than one. /// For registers, it merges the registers and returns the merged record. pub async fn get_record_from_network( &self, @@ -597,7 +596,7 @@ impl Network { } /// Handle the split record error. - /// Spend: Accumulate spends and return error if more than one. + /// Spend: Accumulate transactions and return error if more than one. /// Register: Merge registers and return the merged record. fn handle_split_record_error( result_map: &HashMap)>, @@ -605,9 +604,9 @@ impl Network { ) -> std::result::Result, NetworkError> { let pretty_key = PrettyPrintRecordKey::from(key); - // attempt to deserialise and accumulate any spends or registers + // attempt to deserialise and accumulate any transactions or registers let results_count = result_map.len(); - let mut accumulated_spends = HashSet::new(); + let mut accumulated_transactions = HashSet::new(); let mut collected_registers = Vec::new(); let mut valid_scratchpad: Option = None; @@ -634,12 +633,12 @@ impl Network { error!("Encountered a split record for {pretty_key:?} with unexpected RecordKind {kind:?}, skipping."); continue; } - RecordKind::Spend => { - info!("For record {pretty_key:?}, we have a split record for a spend attempt. Accumulating spends"); + RecordKind::Transaction => { + info!("For record {pretty_key:?}, we have a split record for a transaction attempt. Accumulating transactions"); - match get_raw_signed_spends_from_record(record) { - Ok(spends) => { - accumulated_spends.extend(spends); + match get_transactions_from_record(record) { + Ok(transactions) => { + accumulated_transactions.extend(transactions); } Err(_) => { continue; @@ -702,11 +701,13 @@ impl Network { } // Allow for early bail if we've already seen a split SpendAttempt - if accumulated_spends.len() > 1 { - info!("For record {pretty_key:?} task found split record for a spend, accumulated and sending them as a single record"); - let accumulated_spends = accumulated_spends.into_iter().collect::>(); + if accumulated_transactions.len() > 1 { + info!("For record {pretty_key:?} task found split record for a transaction, accumulated and sending them as a single record"); + let accumulated_transactions = accumulated_transactions + .into_iter() + .collect::>(); - return Err(NetworkError::DoubleSpendAttempt(accumulated_spends)); + return Err(NetworkError::DoubleSpendAttempt(accumulated_transactions)); } else if !collected_registers.is_empty() { info!("For record {pretty_key:?} task found multiple registers, merging them."); let signed_register = collected_registers.iter().fold(collected_registers[0].clone(), |mut acc, x| { diff --git a/sn_networking/src/record_store.rs b/sn_networking/src/record_store.rs index 01df011fe4..ea26b8f9ce 100644 --- a/sn_networking/src/record_store.rs +++ b/sn_networking/src/record_store.rs @@ -47,9 +47,9 @@ use tokio::sync::mpsc; use walkdir::{DirEntry, WalkDir}; use xor_name::XorName; -// A spend record is at the size of 4KB roughly. +// A transaction record is at the size of 4KB roughly. // Given chunk record is maxed at size of 4MB. -// During Beta phase, it's almost one spend per chunk, +// During Beta phase, it's almost one transaction per chunk, // which makes the average record size is around 2MB. // Given we are targeting node size to be 32GB, // this shall allow around 16K records. @@ -835,7 +835,7 @@ impl RecordStore for NodeRecordStore { // Chunk with existing key do not to be stored again. // `Spend` or `Register` with same content_hash do not to be stored again, // otherwise shall be passed further to allow - // double spend to be detected or register op update. + // double transaction to be detected or register op update. match self.records.get(&record.key) { Some((_addr, RecordType::Chunk)) => { debug!("Chunk {record_key:?} already exists."); diff --git a/sn_networking/src/replication_fetcher.rs b/sn_networking/src/replication_fetcher.rs index 58b031c07c..6eae465b5f 100644 --- a/sn_networking/src/replication_fetcher.rs +++ b/sn_networking/src/replication_fetcher.rs @@ -361,7 +361,7 @@ impl ReplicationFetcher { } /// Remove keys that we hold already and no longer need to be replicated. - /// This checks the hash on spends to ensure we pull in divergent spends. + /// This checks the hash on transactions to ensure we pull in divergent transactions. fn remove_stored_keys( &mut self, existing_keys: &HashMap, diff --git a/sn_networking/src/spends.rs b/sn_networking/src/spends.rs deleted file mode 100644 index 3c4ce74f07..0000000000 --- a/sn_networking/src/spends.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{Network, NetworkError, Result}; -use futures::future::join_all; -use sn_transfers::{is_genesis_spend, SignedSpend, SpendAddress, TransferError}; -use std::collections::BTreeSet; - -impl Network { - /// This function verifies a single spend. - /// This is used by nodes for spends validation, before storing them. - /// - It checks if the spend has valid ancestry, that its parents exist on the Network. - /// - If the parent is a double spend, we still carry out the valdiation, but at the end return the error - /// - It checks that the spend has a valid signature and content - /// - It does NOT check if the spend exists online - /// - It does NOT check if the spend is already spent on the Network - pub async fn verify_spend(&self, spend: &SignedSpend) -> Result<()> { - let unique_key = spend.unique_pubkey(); - debug!("Verifying spend {unique_key}"); - spend.verify()?; - - // genesis does not have parents so we end here - if is_genesis_spend(spend) { - debug!("Verified {unique_key} was Genesis spend!"); - return Ok(()); - } - - // get its parents - let mut result = Ok(()); - let parent_keys = spend.spend.ancestors.clone(); - let tasks: Vec<_> = parent_keys - .iter() - .map(|parent| async move { - let spend = self - .get_spend(SpendAddress::from_unique_pubkey(parent)) - .await; - (*parent, spend) - }) - .collect(); - let mut parent_spends = BTreeSet::new(); - for (parent_key, parent_spend) in join_all(tasks).await { - match parent_spend { - Ok(parent_spend) => { - parent_spends.insert(parent_spend); - } - Err(NetworkError::DoubleSpendAttempt(attempts)) => { - warn!("While verifying {unique_key:?}, a double spend attempt ({attempts:?}) detected for the parent with pub key {parent_key:?} . Continuing verification."); - parent_spends.extend(attempts); - result = Err(NetworkError::Transfer(TransferError::DoubleSpentParent)); - } - Err(e) => { - let s = format!("Failed to get parent spend of {unique_key} parent pubkey: {parent_key:?} error: {e}"); - warn!("{}", s); - return Err(NetworkError::Transfer(TransferError::InvalidParentSpend(s))); - } - } - } - - // verify the parents - spend.verify_parent_spends(&parent_spends)?; - - result - } -} diff --git a/sn_networking/src/transactions.rs b/sn_networking/src/transactions.rs new file mode 100644 index 0000000000..0abdf8dedc --- /dev/null +++ b/sn_networking/src/transactions.rs @@ -0,0 +1,50 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use crate::{driver::GetRecordCfg, Network, NetworkError, Result}; +use libp2p::kad::{Quorum, Record}; +use sn_protocol::storage::{Transaction, TransactionAddress}; +use sn_protocol::{ + storage::{try_deserialize_record, RecordHeader, RecordKind, RetryStrategy}, + NetworkAddress, PrettyPrintRecordKey, +}; + +impl Network { + /// Gets Transactions at TransactionAddress from the Network. + pub async fn get_transactions(&self, address: TransactionAddress) -> Result> { + let key = NetworkAddress::from_transaction_address(address).to_record_key(); + let get_cfg = GetRecordCfg { + get_quorum: Quorum::All, + retry_strategy: Some(RetryStrategy::Quick), + target_record: None, + expected_holders: Default::default(), + is_register: false, + }; + let record = self.get_record_from_network(key.clone(), &get_cfg).await?; + debug!( + "Got record from the network, {:?}", + PrettyPrintRecordKey::from(&record.key) + ); + + get_transactions_from_record(&record) + } +} + +pub fn get_transactions_from_record(record: &Record) -> Result> { + let header = RecordHeader::from_record(record)?; + if let RecordKind::Transaction = header.kind { + let transactions = try_deserialize_record::>(record)?; + Ok(transactions) + } else { + warn!( + "RecordKind mismatch while trying to retrieve transactions from record {:?}", + PrettyPrintRecordKey::from(&record.key) + ); + Err(NetworkError::RecordKindMismatch(RecordKind::Transaction)) + } +} diff --git a/sn_networking/src/transfers.rs b/sn_networking/src/transfers.rs deleted file mode 100644 index 76b6349ce1..0000000000 --- a/sn_networking/src/transfers.rs +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{ - close_group_majority, driver::GetRecordCfg, GetRecordError, Network, NetworkError, Result, -}; -use libp2p::kad::{Quorum, Record}; -use sn_protocol::{ - storage::{try_deserialize_record, RecordHeader, RecordKind, RetryStrategy, SpendAddress}, - NetworkAddress, PrettyPrintRecordKey, -}; -use sn_transfers::{CashNote, CashNoteRedemption, HotWallet, MainPubkey, SignedSpend, Transfer}; -use std::collections::BTreeSet; -use tokio::task::JoinSet; - -impl Network { - /// Gets raw spends from the Network. - /// For normal use please prefer using `get_spend` instead. - /// Double spends returned together as is, not as an error. - /// The target may have high chance not present in the network yet. - /// - /// If we get a quorum error, we enable re-try - pub async fn get_raw_spends(&self, address: SpendAddress) -> Result> { - let key = NetworkAddress::from_spend_address(address).to_record_key(); - let get_cfg = GetRecordCfg { - get_quorum: Quorum::Majority, - retry_strategy: None, - // This should not be set here. This function is used as a quick check to find the spends around the key during - // validation. The returned records might possibly be double spend attempt and the record will not match - // what we will have in hand. - target_record: None, - expected_holders: Default::default(), - is_register: false, - }; - let record = self.get_record_from_network(key.clone(), &get_cfg).await?; - debug!( - "Got record from the network, {:?}", - PrettyPrintRecordKey::from(&record.key) - ); - get_raw_signed_spends_from_record(&record) - } - - /// Gets a spend from the Network. - /// We know it must be there, and has to be fetched from Quorum::All - /// - /// If we get a quorum error, we increase the RetryStrategy - pub async fn get_spend(&self, address: SpendAddress) -> Result { - let key = NetworkAddress::from_spend_address(address).to_record_key(); - let mut get_cfg = GetRecordCfg { - get_quorum: Quorum::All, - retry_strategy: Some(RetryStrategy::Quick), - target_record: None, - expected_holders: Default::default(), - is_register: false, - }; - let record = match self.get_record_from_network(key.clone(), &get_cfg).await { - Ok(record) => record, - Err(NetworkError::GetRecordError(GetRecordError::NotEnoughCopies { - record, - expected, - got, - })) => { - // if majority holds the spend, it might be worth to be trusted. - if got >= close_group_majority() { - debug!("At least a majority nodes hold the spend {address:?}, going to trust it if can fetch with majority again."); - get_cfg.get_quorum = Quorum::Majority; - get_cfg.retry_strategy = Some(RetryStrategy::Balanced); - self.get_record_from_network(key, &get_cfg).await? - } else { - return Err(NetworkError::GetRecordError( - GetRecordError::NotEnoughCopies { - record, - expected, - got, - }, - )); - } - } - Err(err) => return Err(err), - }; - debug!( - "Got record from the network, {:?}", - PrettyPrintRecordKey::from(&record.key) - ); - - get_signed_spend_from_record(&address, &record) - } - - /// This function is used to receive a Transfer and turn it back into spendable CashNotes. - /// Needs Network connection. - /// Verify Transfer and rebuild spendable currency from it - /// Returns an `Error::FailedToDecypherTransfer` if the transfer cannot be decyphered - /// (This means the transfer is not for us as it was not encrypted to our key) - /// Returns an `Error::InvalidTransfer` if the transfer is not valid - /// Else returns a list of CashNotes that can be deposited to our wallet and spent - pub async fn verify_and_unpack_transfer( - &self, - transfer: &Transfer, - wallet: &HotWallet, - ) -> Result> { - // get CashNoteRedemptions from encrypted Transfer - debug!("Decyphering Transfer"); - let cashnote_redemptions = wallet.unwrap_transfer(transfer)?; - - self.verify_cash_notes_redemptions(wallet.address(), &cashnote_redemptions) - .await - } - - /// This function is used to receive a list of CashNoteRedemptions and turn it back into spendable CashNotes. - /// Needs Network connection. - /// Verify CashNoteRedemptions and rebuild spendable currency from them. - /// Returns an `Error::InvalidTransfer` if any CashNoteRedemption is not valid - /// Else returns a list of CashNotes that can be spent by the owner. - pub async fn verify_cash_notes_redemptions( - &self, - main_pubkey: MainPubkey, - cashnote_redemptions: &[CashNoteRedemption], - ) -> Result> { - // get all the parent spends - debug!( - "Getting parent spends for validation from {:?}", - cashnote_redemptions.len() - ); - let parent_addrs: BTreeSet = cashnote_redemptions - .iter() - .flat_map(|u| u.parent_spends.clone()) - .collect(); - let mut tasks = JoinSet::new(); - for addr in parent_addrs.clone() { - let self_clone = self.clone(); - let _ = tasks.spawn(async move { self_clone.get_spend(addr).await }); - } - let mut parent_spends = BTreeSet::new(); - while let Some(result) = tasks.join_next().await { - let signed_spend = result - .map_err(|e| NetworkError::FailedToGetSpend(format!("{e}")))? - .map_err(|e| NetworkError::InvalidTransfer(format!("{e}")))?; - let _ = parent_spends.insert(signed_spend.clone()); - } - - // get our outputs CashNotes - let our_output_cash_notes: Vec = cashnote_redemptions - .iter() - .map(|cnr| { - let derivation_index = cnr.derivation_index; - // assuming parent spends all exist as they were collected just above - let parent_spends: BTreeSet = cnr - .parent_spends - .iter() - .flat_map(|a| { - parent_spends - .iter() - .find(|s| &s.address() == a) - .map(|s| vec![s]) - .unwrap_or_default() - }) - .cloned() - .collect(); - - CashNote { - parent_spends: parent_spends.clone(), - main_pubkey, - derivation_index, - } - }) - .collect(); - - // verify our output cash notes - for cash_note in our_output_cash_notes.iter() { - cash_note.verify().map_err(|e| { - NetworkError::InvalidTransfer(format!("Invalid CashNoteRedemption: {e}")) - })?; - } - - Ok(our_output_cash_notes) - } -} - -/// Tries to get the signed spend out of a record as is, double spends are returned together as is. -pub fn get_raw_signed_spends_from_record(record: &Record) -> Result> { - let header = RecordHeader::from_record(record)?; - if let RecordKind::Spend = header.kind { - let spends = try_deserialize_record::>(record)?; - Ok(spends) - } else { - warn!( - "RecordKind mismatch while trying to retrieve spends from record {:?}", - PrettyPrintRecordKey::from(&record.key) - ); - Err(NetworkError::RecordKindMismatch(RecordKind::Spend)) - } -} - -/// Get the signed spend out of a record. -/// Double spends are returned as an error -pub fn get_signed_spend_from_record( - address: &SpendAddress, - record: &Record, -) -> Result { - let spends = get_raw_signed_spends_from_record(record)?; - match spends.as_slice() { - [] => { - error!("Found no spend for {address:?}"); - Err(NetworkError::NoSpendFoundInsideRecord(*address)) - } - [one] => { - debug!("Spend get for address: {address:?} successful"); - Ok(one.clone()) - } - _double_spends => { - warn!( - "Found double spend(s) of len {} for {address:?}", - spends.len() - ); - Err(NetworkError::DoubleSpendAttempt(spends)) - } - } -} diff --git a/sn_node/Cargo.toml b/sn_node/Cargo.toml index 9e5ebaaa51..4675199847 100644 --- a/sn_node/Cargo.toml +++ b/sn_node/Cargo.toml @@ -60,7 +60,6 @@ sn_logging = { path = "../sn_logging", version = "0.2.40" } sn_networking = { path = "../sn_networking", version = "0.19.5" } sn_protocol = { path = "../sn_protocol", version = "0.17.15" } sn_registers = { path = "../sn_registers", version = "0.4.3" } -sn_transfers = { path = "../sn_transfers", version = "0.20.3" } sn_service_management = { path = "../sn_service_management", version = "0.4.3" } sn_evm = { path = "../sn_evm", version = "0.1.4" } sysinfo = { version = "0.30.8", default-features = false } @@ -96,9 +95,6 @@ serde_json = "1.0" sn_protocol = { path = "../sn_protocol", version = "0.17.15", features = [ "rpc", ] } -sn_transfers = { path = "../sn_transfers", version = "0.20.3", features = [ - "test-utils", -] } tempfile = "3.6.0" # Do not specify the version field. Release process expects even the local dev deps to be published. # Removing the version field is a workaround. diff --git a/sn_node/README.md b/sn_node/README.md index 890e2e8b28..414a94c38f 100644 --- a/sn_node/README.md +++ b/sn_node/README.md @@ -120,7 +120,7 @@ default_dir = SafeNode.get_default_root_dir(peer_id) - `get_validation.rs`: Validation for GET requests - `put_validation.rs`: Validation for PUT requests - `replication.rs`: Data replication logic - - `spends.rs`: Logic related to spending tokens or resources + - `transactions.rs`: Logic related to transactioning tokens or resources - `tests/`: Test files - `common/mod.rs`: Common utilities for tests - `data_with_churn.rs`: Tests related to data with churn diff --git a/sn_node/src/bin/safenode/main.rs b/sn_node/src/bin/safenode/main.rs index 29fcd0b501..385f9a52e7 100644 --- a/sn_node/src/bin/safenode/main.rs +++ b/sn_node/src/bin/safenode/main.rs @@ -561,7 +561,6 @@ fn init_logging(opt: &Opt, peer_id: PeerId) -> Result<(String, ReloadHandle, Opt ("sn_peers_acquisition".to_string(), Level::DEBUG), ("sn_protocol".to_string(), Level::DEBUG), ("sn_registers".to_string(), Level::DEBUG), - ("sn_transfers".to_string(), Level::DEBUG), ("sn_evm".to_string(), Level::DEBUG), ]; diff --git a/sn_node/src/error.rs b/sn_node/src/error.rs index a74ed00bc7..a36f742864 100644 --- a/sn_node/src/error.rs +++ b/sn_node/src/error.rs @@ -8,7 +8,6 @@ use sn_evm::AttoTokens; use sn_protocol::{NetworkAddress, PrettyPrintRecordKey}; -use sn_transfers::WalletError; use thiserror::Error; pub(super) type Result = std::result::Result; @@ -26,9 +25,6 @@ pub enum Error { #[error("Register error {0}")] Register(#[from] sn_registers::Error), - #[error("WalletError error {0}")] - Wallet(#[from] WalletError), - #[error("Transfers Error {0}")] Transfers(#[from] sn_evm::EvmError), diff --git a/sn_node/src/log_markers.rs b/sn_node/src/log_markers.rs index ac68e5ae89..7d8017c501 100644 --- a/sn_node/src/log_markers.rs +++ b/sn_node/src/log_markers.rs @@ -42,7 +42,7 @@ pub enum Marker<'a> { /// Valid non-existing Register record PUT from the network received and stored ValidRegisterRecordPutFromNetwork(&'a PrettyPrintRecordKey<'a>), /// Valid non-existing Spend record PUT from the network received and stored - ValidSpendRecordPutFromNetwork(&'a PrettyPrintRecordKey<'a>), + ValidTransactionRecordPutFromNetwork(&'a PrettyPrintRecordKey<'a>), /// Valid Scratchpad record PUT from the network received and stored ValidScratchpadRecordPutFromNetwork(&'a PrettyPrintRecordKey<'a>), @@ -50,7 +50,7 @@ pub enum Marker<'a> { ValidPaidChunkPutFromClient(&'a PrettyPrintRecordKey<'a>), /// Valid paid to us and royalty paid register stored ValidPaidRegisterPutFromClient(&'a PrettyPrintRecordKey<'a>), - /// Valid spend stored + /// Valid transaction stored ValidSpendPutFromClient(&'a PrettyPrintRecordKey<'a>), /// Valid scratchpad stored ValidScratchpadRecordPutFromClient(&'a PrettyPrintRecordKey<'a>), diff --git a/sn_node/src/metrics.rs b/sn_node/src/metrics.rs index 83ae86e4d6..3aac27c02f 100644 --- a/sn_node/src/metrics.rs +++ b/sn_node/src/metrics.rs @@ -171,7 +171,7 @@ impl NodeMetricsRecorder { .inc(); } - Marker::ValidSpendRecordPutFromNetwork(_) => { + Marker::ValidTransactionRecordPutFromNetwork(_) => { let _ = self .put_record_ok .get_or_create(&PutRecordOk { diff --git a/sn_node/src/put_validation.rs b/sn_node/src/put_validation.rs index d08e1e7d28..bac5117eb4 100644 --- a/sn_node/src/put_validation.rs +++ b/sn_node/src/put_validation.rs @@ -8,20 +8,18 @@ use crate::{node::Node, Error, Marker, Result}; use libp2p::kad::{Record, RecordKey}; -use sn_evm::ProofOfPayment; -use sn_networking::{get_raw_signed_spends_from_record, GetRecordError, NetworkError}; +use sn_evm::{ProofOfPayment, QUOTE_EXPIRATION_SECS}; +use sn_networking::NetworkError; +use sn_protocol::storage::Transaction; use sn_protocol::{ storage::{ try_deserialize_record, try_serialize_record, Chunk, RecordHeader, RecordKind, RecordType, - Scratchpad, SpendAddress, + Scratchpad, TransactionAddress, }, NetworkAddress, PrettyPrintRecordKey, }; use sn_registers::SignedRegister; -use sn_transfers::{SignedSpend, TransferError, UniquePubkey, QUOTE_EXPIRATION_SECS}; -use std::collections::BTreeSet; use std::time::{Duration, UNIX_EPOCH}; -use tokio::task::JoinSet; use xor_name::XorName; impl Node { @@ -154,12 +152,12 @@ impl Node { self.validate_and_store_scratchpad_record(scratchpad, key, false) .await } - RecordKind::Spend => { + RecordKind::Transaction => { let record_key = record.key.clone(); let value_to_hash = record.value.clone(); - let spends = try_deserialize_record::>(&record)?; + let transactions = try_deserialize_record::>(&record)?; let result = self - .validate_merge_and_store_spends(spends, &record_key) + .validate_merge_and_store_transactions(transactions, &record_key) .await; if result.is_ok() { Marker::ValidSpendPutFromClient(&PrettyPrintRecordKey::from(&record_key)).log(); @@ -305,10 +303,10 @@ impl Node { self.validate_and_store_scratchpad_record(scratchpad, key, false) .await } - RecordKind::Spend => { + RecordKind::Transaction => { let record_key = record.key.clone(); - let spends = try_deserialize_record::>(&record)?; - self.validate_merge_and_store_spends(spends, &record_key) + let transactions = try_deserialize_record::>(&record)?; + self.validate_merge_and_store_transactions(transactions, &record_key) .await } RecordKind::Register => { @@ -508,85 +506,79 @@ impl Node { Ok(()) } - /// Validate and store `Vec` to the RecordStore - /// If we already have a spend at this address, the Vec is extended and stored. - pub(crate) async fn validate_merge_and_store_spends( + /// Validate and store `Vec` to the RecordStore + /// If we already have a transaction at this address, the Vec is extended and stored. + pub(crate) async fn validate_merge_and_store_transactions( &self, - signed_spends: Vec, + transactions: Vec, record_key: &RecordKey, ) -> Result<()> { let pretty_key = PrettyPrintRecordKey::from(record_key); - debug!("Validating spends before storage at {pretty_key:?}"); + debug!("Validating transactions before storage at {pretty_key:?}"); - // only keep spends that match the record key - let spends_for_key: Vec = signed_spends + // only keep transactions that match the record key + let transactions_for_key: Vec = transactions .into_iter() .filter(|s| { - // get the record key for the spend - let spend_address = SpendAddress::from_unique_pubkey(s.unique_pubkey()); - let network_address = NetworkAddress::from_spend_address(spend_address); - let spend_record_key = network_address.to_record_key(); - let spend_pretty = PrettyPrintRecordKey::from(&spend_record_key); - if &spend_record_key != record_key { - warn!("Ignoring spend for another record key {spend_pretty:?} when verifying: {pretty_key:?}"); + // get the record key for the transaction + let transaction_address = s.address(); + let network_address = NetworkAddress::from_transaction_address(transaction_address); + let transaction_record_key = network_address.to_record_key(); + let transaction_pretty = PrettyPrintRecordKey::from(&transaction_record_key); + if &transaction_record_key != record_key { + warn!("Ignoring transaction for another record key {transaction_pretty:?} when verifying: {pretty_key:?}"); return false; } true }) .collect(); - // if we have no spends to verify, return early - let unique_pubkey = match spends_for_key.as_slice() { - [] => { - warn!("Found no valid spends to verify upon validation for {pretty_key:?}"); - return Err(Error::InvalidRequest(format!( - "No spends to verify when validating {pretty_key:?}" - ))); - } - [a, ..] => { - // they should all have the same unique_pubkey so we take the 1st one - a.unique_pubkey() - } - }; + // if we have no transactions to verify, return early + if transactions_for_key.is_empty() { + warn!("Found no valid transactions to verify upon validation for {pretty_key:?}"); + return Err(Error::InvalidRequest(format!( + "No transactions to verify when validating {pretty_key:?}" + ))); + } - // validate the signed spends against the network and the local knowledge - debug!("Validating spends for {pretty_key:?} with unique key: {unique_pubkey:?}"); - let validated_spends = match self - .signed_spends_to_keep(spends_for_key.clone(), *unique_pubkey) - .await - { - Ok((one, None)) => vec![one], - Ok((one, Some(two))) => vec![one, two], - Err(e) => { - warn!("Failed to validate spends at {pretty_key:?} with unique key {unique_pubkey:?}: {e}"); - return Err(e); + // verify the transactions + let mut validated_transactions: Vec = transactions_for_key + .into_iter() + .filter(|t| t.verify()) + .collect(); + + // skip if none are valid + let addr = match validated_transactions.as_slice() { + [] => { + warn!("Found no validated transactions to store at {pretty_key:?}"); + return Ok(()); } + [t, ..] => t.address(), }; - debug!( - "Got {} validated spends with key: {unique_pubkey:?} at {pretty_key:?}", - validated_spends.len() - ); + // add local transactions to the validated transactions + let local_txs = self.get_local_transactions(addr).await?; + validated_transactions.extend(local_txs); // store the record into the local storage let record = Record { key: record_key.clone(), - value: try_serialize_record(&validated_spends, RecordKind::Spend)?.to_vec(), + value: try_serialize_record(&validated_transactions, RecordKind::Transaction)?.to_vec(), publisher: None, expires: None, }; self.network().put_local_record(record); - debug!( - "Successfully stored validated spends with key: {unique_pubkey:?} at {pretty_key:?}" - ); + debug!("Successfully stored validated transactions at {pretty_key:?}"); - // Just log the double spend attempt. DoubleSpend error during PUT is not used and would just lead to - // RecordRejected marker (which is incorrect, since we store double spends). - if validated_spends.len() > 1 { - warn!("Got double spend(s) of len {} for the Spend PUT with unique_pubkey {unique_pubkey}", validated_spends.len()); + // Just log the multiple transactions + if validated_transactions.len() > 1 { + debug!( + "Got multiple transaction(s) of len {} at {pretty_key:?}", + validated_transactions.len() + ); } - self.record_metrics(Marker::ValidSpendRecordPutFromNetwork(&pretty_key)); + self.record_metrics(Marker::ValidTransactionRecordPutFromNetwork(&pretty_key)); Ok(()) } @@ -710,235 +702,28 @@ impl Node { } } - /// Get the local spends for the provided `SpendAddress` - /// This only fetches the spends from the local store and does not perform any network operations. - async fn get_local_spends(&self, addr: SpendAddress) -> Result> { - // get the local spends - let record_key = NetworkAddress::from_spend_address(addr).to_record_key(); - debug!("Checking for local spends with key: {record_key:?}"); + /// Get the local transactions for the provided `TransactionAddress` + /// This only fetches the transactions from the local store and does not perform any network operations. + async fn get_local_transactions(&self, addr: TransactionAddress) -> Result> { + // get the local transactions + let record_key = NetworkAddress::from_transaction_address(addr).to_record_key(); + debug!("Checking for local transactions with key: {record_key:?}"); let local_record = match self.network().get_local_record(&record_key).await? { Some(r) => r, None => { - debug!("Spend is not present locally: {record_key:?}"); + debug!("Transaction is not present locally: {record_key:?}"); return Ok(vec![]); } }; - // deserialize the record and get the spends + // deserialize the record and get the transactions let local_header = RecordHeader::from_record(&local_record)?; let record_kind = local_header.kind; - if !matches!(record_kind, RecordKind::Spend) { + if !matches!(record_kind, RecordKind::Transaction) { error!("Found a {record_kind} when expecting to find Spend at {addr:?}"); - return Err(NetworkError::RecordKindMismatch(RecordKind::Spend).into()); - } - let local_signed_spends: Vec = try_deserialize_record(&local_record)?; - Ok(local_signed_spends) - } - - /// Determine which spends our node should keep and store - /// - get local spends and trust them - /// - get spends from the network - /// - verify incoming spend + network spends and ignore the invalid ones - /// - orders all the verified spends by: - /// - if they have spent descendants (meaning live branch) - /// - deterministicaly by their order in the BTreeSet - /// - returns the spend to keep along with another spend if it was a double spend - /// - when we get more than two spends, only keeps 2 that are chosen deterministically so - /// all nodes running this code are eventually consistent - async fn signed_spends_to_keep( - &self, - signed_spends: Vec, - unique_pubkey: UniquePubkey, - ) -> Result<(SignedSpend, Option)> { - let spend_addr = SpendAddress::from_unique_pubkey(&unique_pubkey); - debug!( - "Validating before storing spend at {spend_addr:?} with unique key: {unique_pubkey}" - ); - - // trust local spends as we've verified them before - let local_spends = self.get_local_spends(spend_addr).await?; - - // get spends from the network at the address for that unique pubkey - let network_spends = match self.network().get_raw_spends(spend_addr).await { - Ok(spends) => spends, - // Fixme: We don't return SplitRecord Error for spends, instead we return NetworkError::DoubleSpendAttempt. - // The fix should also consider/change all the places we try to get spends, for eg `get_raw_signed_spends_from_record` etc. - Err(NetworkError::GetRecordError(GetRecordError::SplitRecord { result_map })) => { - warn!("Got a split record (double spend) for {unique_pubkey:?} from the network"); - let mut spends = vec![]; - for (record, _) in result_map.values() { - match get_raw_signed_spends_from_record(record) { - Ok(s) => spends.extend(s), - Err(e) => warn!("Ignoring invalid record received from the network for spend: {unique_pubkey:?}: {e}"), - } - } - spends - } - Err(NetworkError::GetRecordError(GetRecordError::NotEnoughCopies { - record, - got, - .. - })) => { - info!( - "Retrieved {got} copies of the record for {unique_pubkey:?} from the network" - ); - match get_raw_signed_spends_from_record(&record) { - Ok(spends) => spends, - Err(err) => { - warn!("Ignoring invalid record received from the network for spend: {unique_pubkey:?}: {err}"); - vec![] - } - } - } - - Err(e) => { - warn!("Continuing without network spends as failed to get spends from the network for {unique_pubkey:?}: {e}"); - vec![] - } - }; - debug!( - "For {unique_pubkey:?} got {} local spends, {} from network and {} provided", - local_spends.len(), - network_spends.len(), - signed_spends.len() - ); - debug!("Local spends {local_spends:?}; from network {network_spends:?}; provided {signed_spends:?}"); - - // only verify spends we don't know of - let mut all_verified_spends = BTreeSet::from_iter(local_spends.into_iter()); - let unverified_spends = - BTreeSet::from_iter(network_spends.into_iter().chain(signed_spends.into_iter())); - let known_spends = all_verified_spends.clone(); - let new_unverified_spends: BTreeSet<_> = - unverified_spends.difference(&known_spends).collect(); - - let mut tasks = JoinSet::new(); - for s in new_unverified_spends.into_iter() { - let self_clone = self.clone(); - let spend_clone = s.clone(); - let _ = tasks.spawn(async move { - let res = self_clone.network().verify_spend(&spend_clone).await; - (spend_clone, res) - }); - } - - // gather verified spends - let mut double_spent_parent = BTreeSet::new(); - while let Some(res) = tasks.join_next().await { - match res { - Ok((spend, Ok(()))) => { - info!("Successfully verified {spend:?}"); - let _inserted = all_verified_spends.insert(spend.to_owned().clone()); - } - Ok((spend, Err(NetworkError::Transfer(TransferError::DoubleSpentParent)))) => { - warn!("Parent of {spend:?} was double spent, keeping aside in case we're a double spend as well"); - let _ = double_spent_parent.insert(spend.clone()); - } - Ok((spend, Err(e))) => { - // an error here most probably means the received spend is invalid - warn!("Skipping spend {spend:?} as an error occurred during validation: {e:?}"); - } - Err(e) => { - let s = - format!("Async thread error while verifying spend {unique_pubkey}: {e:?}"); - error!("{}", s); - return Err(Error::JoinErrorInAsyncThread(s))?; - } - } - } - - // keep track of double spend with double spent parent - if !all_verified_spends.is_empty() && !double_spent_parent.is_empty() { - warn!("Parent of {unique_pubkey:?} was double spent, but it's also a double spend. So keeping track of this double spend attempt."); - all_verified_spends.extend(double_spent_parent.into_iter()) - } - - // return 2 spends max - let all_verified_spends: Vec<_> = all_verified_spends.into_iter().collect(); - match all_verified_spends.as_slice() { - [one_spend] => Ok((one_spend.clone(), None)), - [one, two] => Ok((one.clone(), Some(two.clone()))), - [] => { - warn!("Invalid request: none of the spends were valid for {unique_pubkey:?}"); - Err(Error::InvalidRequest(format!( - "Found no valid spends while validating Spends for {unique_pubkey:?}" - ))) - } - more => { - warn!("Got more than 2 verified spends, this might be a double spend spam attack, making sure to favour live branches (branches with spent descendants)"); - let (one, two) = self.verified_spends_select_2_live(more).await?; - Ok((one, Some(two))) - } - } - } - - async fn verified_spends_select_2_live( - &self, - many_spends: &[SignedSpend], - ) -> Result<(SignedSpend, SignedSpend)> { - // get all spends descendants - let mut tasks = JoinSet::new(); - for spend in many_spends { - let descendants: BTreeSet<_> = spend - .spend - .descendants - .keys() - .map(SpendAddress::from_unique_pubkey) - .collect(); - for d in descendants { - let self_clone = self.clone(); - let spend_clone = spend.to_owned(); - let _ = tasks.spawn(async move { - let res = self_clone.network().get_raw_spends(d).await; - (spend_clone, res) - }); - } - } - - // identify up to two live spends (aka spends with spent descendants) - let mut live_spends = BTreeSet::new(); - while let Some(res) = tasks.join_next().await { - match res { - Ok((spend, Ok(_descendant))) => { - debug!("Spend {spend:?} has a live descendant"); - let _inserted = live_spends.insert(spend); - } - Ok((spend, Err(NetworkError::GetRecordError(GetRecordError::RecordNotFound)))) => { - debug!("Spend {spend:?} descendant was not found, continuing..."); - } - Ok((spend, Err(e))) => { - warn!( - "Error fetching spend descendant while checking if {spend:?} is live: {e}" - ); - } - Err(e) => { - let s = format!("Async thread error while selecting live spends: {e}"); - error!("{}", s); - return Err(Error::JoinErrorInAsyncThread(s))?; - } - } - } - - // order by live or not live, then order in the BTreeSet and take first 2 - let not_live_spends: BTreeSet<_> = many_spends - .iter() - .filter(|s| !live_spends.contains(s)) - .collect(); - debug!( - "Got {} live spends and {} not live ones, keeping only the favoured 2", - live_spends.len(), - not_live_spends.len() - ); - let ordered_spends: Vec<_> = live_spends - .iter() - .chain(not_live_spends.into_iter()) - .collect(); - match ordered_spends.as_slice() { - [one, two, ..] => Ok((one.to_owned().clone(), two.to_owned().clone())), - _ => Err(Error::InvalidRequest(format!( - "Expected many spends but got {}", - many_spends.len() - ))), + return Err(NetworkError::RecordKindMismatch(RecordKind::Transaction).into()); } + let local_transactions: Vec = try_deserialize_record(&local_record)?; + Ok(local_transactions) } } diff --git a/sn_node/tests/double_spend.rs b/sn_node/tests/double_spend.rs deleted file mode 100644 index 8d06a87187..0000000000 --- a/sn_node/tests/double_spend.rs +++ /dev/null @@ -1,683 +0,0 @@ -// // Copyright 2024 MaidSafe.net limited. -// // -// // This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// // Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// // under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// // KIND, either express or implied. Please review the Licences for the specific language governing -// // permissions and limitations relating to use of the SAFE Network Software. - -// mod common; - -// use assert_fs::TempDir; -// use assert_matches::assert_matches; -// use common::client::{get_client_and_funded_wallet, get_wallet}; -// use eyre::{bail, Result}; -// use itertools::Itertools; -// use sn_transfers::{ -// get_genesis_sk, rng, NanoTokens, DerivationIndex, HotWallet, SignedTransaction, -// SpendReason, WalletError, GENESIS_CASHNOTE, -// }; -// use sn_logging::LogBuilder; -// use sn_networking::NetworkError; -// use std::time::Duration; -// use tracing::*; - -// #[tokio::test] -// async fn cash_note_transfer_double_spend_fail() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("double_spend", true); -// // create 1 wallet add money from faucet -// let first_wallet_dir = TempDir::new()?; - -// let (client, mut first_wallet) = get_client_and_funded_wallet(first_wallet_dir.path()).await?; -// let first_wallet_balance = first_wallet.balance().as_nano(); - -// // create wallet 2 and 3 to receive money from 1 -// let second_wallet_dir = TempDir::new()?; -// let second_wallet = get_wallet(second_wallet_dir.path()); -// assert_eq!(second_wallet.balance(), NanoTokens::zero()); -// let third_wallet_dir = TempDir::new()?; -// let third_wallet = get_wallet(third_wallet_dir.path()); -// assert_eq!(third_wallet.balance(), NanoTokens::zero()); - -// // manually forge two transfers of the same source -// let amount = first_wallet_balance / 3; -// let to1 = first_wallet.address(); -// let to2 = second_wallet.address(); -// let to3 = third_wallet.address(); - -// let (some_cash_notes, _exclusive_access) = first_wallet.available_cash_notes()?; -// let same_cash_notes = some_cash_notes.clone(); - -// let mut rng = rng::thread_rng(); - -// let reason = SpendReason::default(); -// let to2_unique_key = (amount, to2, DerivationIndex::random(&mut rng), false); -// let to3_unique_key = (amount, to3, DerivationIndex::random(&mut rng), false); - -// let transfer_to_2 = SignedTransaction::new( -// some_cash_notes, -// vec![to2_unique_key], -// to1, -// reason.clone(), -// first_wallet.key(), -// )?; -// let transfer_to_3 = SignedTransaction::new( -// same_cash_notes, -// vec![to3_unique_key], -// to1, -// reason, -// first_wallet.key(), -// )?; - -// // send both transfers to the network -// // upload won't error out, only error out during verification. -// info!("Sending both transfers to the network..."); -// let res = client.send_spends(transfer_to_2.spends.iter(), false).await; -// assert!(res.is_ok()); -// let res = client.send_spends(transfer_to_3.spends.iter(), false).await; -// assert!(res.is_ok()); - -// // we wait 5s to ensure that the double spend attempt is detected and accumulated -// info!("Verifying the transfers from first wallet... Sleeping for 10 seconds."); -// tokio::time::sleep(Duration::from_secs(10)).await; - -// let cash_notes_for_2: Vec<_> = transfer_to_2.output_cashnotes.clone(); -// let cash_notes_for_3: Vec<_> = transfer_to_3.output_cashnotes.clone(); - -// // check the CashNotes, it should fail -// let should_err1 = client.verify_cashnote(&cash_notes_for_2[0]).await; -// let should_err2 = client.verify_cashnote(&cash_notes_for_3[0]).await; -// info!("Both should fail during GET record accumulation : {should_err1:?} {should_err2:?}"); -// assert!(should_err1.is_err() && should_err2.is_err()); -// assert_matches!(should_err1, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); -// assert_matches!(should_err2, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); - -// Ok(()) -// } - -// #[tokio::test] -// async fn genesis_double_spend_fail() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("double_spend", true); - -// // create a client and an unused wallet to make sure some money already exists in the system -// let first_wallet_dir = TempDir::new()?; -// let (client, mut first_wallet) = get_client_and_funded_wallet(first_wallet_dir.path()).await?; -// let first_wallet_addr = first_wallet.address(); - -// // create a new genesis wallet with the intention to spend genesis again -// let second_wallet_dir = TempDir::new()?; -// let mut second_wallet = HotWallet::create_from_key(&second_wallet_dir, get_genesis_sk(), None)?; -// second_wallet.deposit_and_store_to_disk(&vec![GENESIS_CASHNOTE.clone()])?; -// let genesis_amount = GENESIS_CASHNOTE.value(); -// let second_wallet_addr = second_wallet.address(); - -// // create a transfer from the second wallet to the first wallet -// // this will spend Genesis (again) and transfer its value to the first wallet -// let (genesis_cashnote, exclusive_access) = second_wallet.available_cash_notes()?; -// let mut rng = rng::thread_rng(); -// let recipient = ( -// genesis_amount, -// first_wallet_addr, -// DerivationIndex::random(&mut rng), -// false, -// ); -// let change_addr = second_wallet_addr; -// let reason = SpendReason::default(); -// let transfer = SignedTransaction::new( -// genesis_cashnote, -// vec![recipient], -// change_addr, -// reason, -// second_wallet.key(), -// )?; - -// // send the transfer to the network which will mark genesis as a double spent -// // making its direct descendants unspendable -// let res = client.send_spends(transfer.spends.iter(), false).await; -// std::mem::drop(exclusive_access); -// assert!(res.is_ok()); - -// // put the bad cashnote in the first wallet -// first_wallet.deposit_and_store_to_disk(&transfer.output_cashnotes)?; - -// // now try to spend this illegitimate cashnote (direct descendant of double spent genesis) -// let (genesis_cashnote_and_others, exclusive_access) = first_wallet.available_cash_notes()?; -// let recipient = ( -// genesis_amount, -// second_wallet_addr, -// DerivationIndex::random(&mut rng), -// false, -// ); -// let bad_genesis_descendant = genesis_cashnote_and_others -// .iter() -// .find(|cn| cn.value() == genesis_amount) -// .unwrap() -// .clone(); -// let change_addr = first_wallet_addr; -// let reason = SpendReason::default(); -// let transfer2 = SignedTransaction::new( -// vec![bad_genesis_descendant], -// vec![recipient], -// change_addr, -// reason, -// first_wallet.key(), -// )?; - -// // send the transfer to the network which should reject it -// let res = client.send_spends(transfer2.spends.iter(), false).await; -// std::mem::drop(exclusive_access); -// assert_matches!(res, Err(WalletError::CouldNotSendMoney(_))); - -// Ok(()) -// } - -// #[tokio::test] -// async fn poisoning_old_spend_should_not_affect_descendant() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("double_spend", true); -// let mut rng = rng::thread_rng(); -// let reason = SpendReason::default(); -// // create 1 wallet add money from faucet -// let wallet_dir_1 = TempDir::new()?; - -// let (client, mut wallet_1) = get_client_and_funded_wallet(wallet_dir_1.path()).await?; -// let balance_1 = wallet_1.balance(); -// let amount = balance_1 / 2; -// let to1 = wallet_1.address(); - -// // Send from 1 -> 2 -// let wallet_dir_2 = TempDir::new()?; -// let mut wallet_2 = get_wallet(wallet_dir_2.path()); -// assert_eq!(wallet_2.balance(), NanoTokens::zero()); - -// let to2 = wallet_2.address(); -// let (cash_notes_1, _exclusive_access) = wallet_1.available_cash_notes()?; -// let to_2_unique_key = (amount, to2, DerivationIndex::random(&mut rng), false); -// let transfer_to_2 = SignedTransaction::new( -// cash_notes_1.clone(), -// vec![to_2_unique_key], -// to1, -// reason.clone(), -// wallet_1.key(), -// )?; - -// info!("Sending 1->2 to the network..."); -// client -// .send_spends(transfer_to_2.spends.iter(), false) -// .await?; - -// info!("Verifying the transfers from 1 -> 2 wallet..."); -// let cash_notes_for_2: Vec<_> = transfer_to_2.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_2[0]).await?; -// wallet_2.deposit_and_store_to_disk(&cash_notes_for_2)?; // store inside 2 - -// // Send from 2 -> 22 -// let wallet_dir_22 = TempDir::new()?; -// let mut wallet_22 = get_wallet(wallet_dir_22.path()); -// assert_eq!(wallet_22.balance(), NanoTokens::zero()); - -// let (cash_notes_2, _exclusive_access) = wallet_2.available_cash_notes()?; -// assert!(!cash_notes_2.is_empty()); -// let to_22_unique_key = ( -// wallet_2.balance(), -// wallet_22.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_22 = SignedTransaction::new( -// cash_notes_2, -// vec![to_22_unique_key], -// to2, -// reason.clone(), -// wallet_2.key(), -// )?; - -// client -// .send_spends(transfer_to_22.spends.iter(), false) -// .await?; - -// info!("Verifying the transfers from 2 -> 22 wallet..."); -// let cash_notes_for_22: Vec<_> = transfer_to_22.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_22[0]).await?; -// wallet_22.deposit_and_store_to_disk(&cash_notes_for_22)?; // store inside 22 - -// // Try to double spend from 1 -> 3 -// let wallet_dir_3 = TempDir::new()?; -// let wallet_3 = get_wallet(wallet_dir_3.path()); -// assert_eq!(wallet_3.balance(), NanoTokens::zero()); - -// let to_3_unique_key = ( -// amount, -// wallet_3.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_3 = SignedTransaction::new( -// cash_notes_1, -// vec![to_3_unique_key], -// to1, -// reason.clone(), -// wallet_1.key(), -// )?; // reuse the old cash notes -// client -// .send_spends(transfer_to_3.spends.iter(), false) -// .await?; -// info!("Verifying the transfers from 1 -> 3 wallet... It should error out."); -// let cash_notes_for_3: Vec<_> = transfer_to_3.output_cashnotes.clone(); -// assert!(client.verify_cashnote(&cash_notes_for_3[0]).await.is_err()); // the old spend has been poisoned -// info!("Verifying the original transfers from 1 -> 2 wallet... It should error out."); -// assert!(client.verify_cashnote(&cash_notes_for_2[0]).await.is_err()); // the old spend has been poisoned - -// // The old spend has been poisoned, but spends from 22 -> 222 should still work -// let wallet_dir_222 = TempDir::new()?; -// let wallet_222 = get_wallet(wallet_dir_222.path()); -// assert_eq!(wallet_222.balance(), NanoTokens::zero()); - -// let (cash_notes_22, _exclusive_access) = wallet_22.available_cash_notes()?; -// assert!(!cash_notes_22.is_empty()); -// let to_222_unique_key = ( -// wallet_22.balance(), -// wallet_222.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_222 = SignedTransaction::new( -// cash_notes_22, -// vec![to_222_unique_key], -// wallet_22.address(), -// reason, -// wallet_22.key(), -// )?; -// client -// .send_spends(transfer_to_222.spends.iter(), false) -// .await?; - -// info!("Verifying the transfers from 22 -> 222 wallet..."); -// let cash_notes_for_222: Vec<_> = transfer_to_222.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_222[0]).await?; - -// // finally assert that we have a double spend attempt error here -// // we wait 1s to ensure that the double spend attempt is detected and accumulated -// tokio::time::sleep(Duration::from_secs(5)).await; - -// match client.verify_cashnote(&cash_notes_for_2[0]).await { -// Ok(_) => bail!("Cashnote verification should have failed"), -// Err(e) => { -// assert!( -// e.to_string() -// .contains("Network Error Double spend(s) attempt was detected"), -// "error should reflect double spend attempt", -// ); -// } -// } - -// match client.verify_cashnote(&cash_notes_for_3[0]).await { -// Ok(_) => bail!("Cashnote verification should have failed"), -// Err(e) => { -// assert!( -// e.to_string() -// .contains("Network Error Double spend(s) attempt was detected"), -// "error should reflect double spend attempt", -// ); -// } -// } -// Ok(()) -// } - -// #[tokio::test] -// /// When A -> B -> C where C is the UTXO cashnote, then double spending A and then double spending B should lead to C -// /// being invalid. -// async fn parent_and_child_double_spends_should_lead_to_cashnote_being_invalid() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("double_spend", true); -// let mut rng = rng::thread_rng(); -// let reason = SpendReason::default(); -// // create 1 wallet add money from faucet -// let wallet_dir_a = TempDir::new()?; - -// let (client, mut wallet_a) = get_client_and_funded_wallet(wallet_dir_a.path()).await?; -// let balance_a = wallet_a.balance().as_nano(); -// let amount = balance_a / 2; - -// // Send from A -> B -// let wallet_dir_b = TempDir::new()?; -// let mut wallet_b = get_wallet(wallet_dir_b.path()); -// assert_eq!(wallet_b.balance(), NanoTokens::zero()); - -// let (cash_notes_a, _exclusive_access) = wallet_a.available_cash_notes()?; -// let to_b_unique_key = ( -// amount, -// wallet_b.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_b = SignedTransaction::new( -// cash_notes_a.clone(), -// vec![to_b_unique_key], -// wallet_a.address(), -// reason.clone(), -// wallet_a.key(), -// )?; - -// info!("Sending A->B to the network..."); -// client -// .send_spends(transfer_to_b.spends.iter(), false) -// .await?; - -// info!("Verifying the transfers from A -> B wallet..."); -// let cash_notes_for_b: Vec<_> = transfer_to_b.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_b[0]).await?; -// wallet_b.deposit_and_store_to_disk(&cash_notes_for_b)?; // store inside B - -// // Send from B -> C -// let wallet_dir_c = TempDir::new()?; -// let mut wallet_c = get_wallet(wallet_dir_c.path()); -// assert_eq!(wallet_c.balance(), NanoTokens::zero()); - -// let (cash_notes_b, _exclusive_access) = wallet_b.available_cash_notes()?; -// assert!(!cash_notes_b.is_empty()); -// let to_c_unique_key = ( -// wallet_b.balance(), -// wallet_c.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_c = SignedTransaction::new( -// cash_notes_b.clone(), -// vec![to_c_unique_key], -// wallet_b.address(), -// reason.clone(), -// wallet_b.key(), -// )?; - -// info!("spend B to C: {:?}", transfer_to_c.spends); -// client -// .send_spends(transfer_to_c.spends.iter(), false) -// .await?; - -// info!("Verifying the transfers from B -> C wallet..."); -// let cash_notes_for_c: Vec<_> = transfer_to_c.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_c[0]).await?; -// wallet_c.deposit_and_store_to_disk(&cash_notes_for_c.clone())?; // store inside c - -// // Try to double spend from A -> X -// let wallet_dir_x = TempDir::new()?; -// let wallet_x = get_wallet(wallet_dir_x.path()); -// assert_eq!(wallet_x.balance(), NanoTokens::zero()); - -// let to_x_unique_key = ( -// amount, -// wallet_x.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_x = SignedTransaction::new( -// cash_notes_a, -// vec![to_x_unique_key], -// wallet_a.address(), -// reason.clone(), -// wallet_a.key(), -// )?; // reuse the old cash notes -// client -// .send_spends(transfer_to_x.spends.iter(), false) -// .await?; -// info!("Verifying the transfers from A -> X wallet... It should error out."); -// let cash_notes_for_x: Vec<_> = transfer_to_x.output_cashnotes.clone(); -// let result = client.verify_cashnote(&cash_notes_for_x[0]).await; -// info!("Got result while verifying double spend from A -> X: {result:?}"); - -// // sleep for a bit to allow the network to process and accumulate the double spend -// tokio::time::sleep(Duration::from_secs(10)).await; - -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); // poisoned - -// // Try to double spend from B -> Y -// let wallet_dir_y = TempDir::new()?; -// let wallet_y = get_wallet(wallet_dir_y.path()); -// assert_eq!(wallet_y.balance(), NanoTokens::zero()); - -// let to_y_unique_key = ( -// amount, -// wallet_y.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_y = SignedTransaction::new( -// cash_notes_b, -// vec![to_y_unique_key], -// wallet_b.address(), -// reason.clone(), -// wallet_b.key(), -// )?; // reuse the old cash notes - -// info!("spend B to Y: {:?}", transfer_to_y.spends); -// client -// .send_spends(transfer_to_y.spends.iter(), false) -// .await?; -// let spend_b_to_y = transfer_to_y.spends.first().expect("should have one"); -// let b_spends = client.get_spend_from_network(spend_b_to_y.address()).await; -// info!("B spends: {b_spends:?}"); - -// info!("Verifying the transfers from B -> Y wallet... It should error out."); -// let cash_notes_for_y: Vec<_> = transfer_to_y.output_cashnotes.clone(); - -// // sleep for a bit to allow the network to process and accumulate the double spend -// tokio::time::sleep(Duration::from_secs(30)).await; - -// let result = client.verify_cashnote(&cash_notes_for_y[0]).await; -// info!("Got result while verifying double spend from B -> Y: {result:?}"); -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); - -// info!("Verifying the original cashnote of A -> B"); -// let result = client.verify_cashnote(&cash_notes_for_b[0]).await; -// info!("Got result while verifying the original spend from A -> B: {result:?}"); -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); - -// info!("Verifying the original cashnote of B -> C"); -// let result = client.verify_cashnote(&cash_notes_for_c[0]).await; -// info!("Got result while verifying the original spend from B -> C: {result:?}"); -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }, "result should be verify error, it was {result:?}"); - -// let result = client.verify_cashnote(&cash_notes_for_y[0]).await; -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }, "result should be verify error, it was {result:?}"); -// let result = client.verify_cashnote(&cash_notes_for_b[0]).await; -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }, "result should be verify error, it was {result:?}"); - -// Ok(()) -// } - -// #[tokio::test] -// /// When A -> B -> C where C is the UTXO cashnote, double spending A many times over and over -// /// should not lead to the original A disappearing and B becoming orphan -// async fn spamming_double_spends_should_not_shadow_live_branch() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("double_spend", true); -// let mut rng = rng::thread_rng(); -// let reason = SpendReason::default(); -// // create 1 wallet add money from faucet -// let wallet_dir_a = TempDir::new()?; - -// let (client, mut wallet_a) = get_client_and_funded_wallet(wallet_dir_a.path()).await?; -// let balance_a = wallet_a.balance(); -// let amount = balance_a / 2; - -// // Send from A -> B -// let wallet_dir_b = TempDir::new()?; -// let mut wallet_b = get_wallet(wallet_dir_b.path()); -// assert_eq!(wallet_b.balance(), NanoTokens::zero()); - -// let (cash_notes_a, _exclusive_access) = wallet_a.available_cash_notes()?; -// let to_b_unique_key = ( -// amount, -// wallet_b.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_b = SignedTransaction::new( -// cash_notes_a.clone(), -// vec![to_b_unique_key], -// wallet_a.address(), -// reason.clone(), -// wallet_a.key(), -// )?; - -// info!("Sending A->B to the network..."); -// client -// .send_spends(transfer_to_b.spends.iter(), false) -// .await?; - -// // save original A spend -// let vec_of_spends = transfer_to_b.spends.into_iter().collect::>(); -// let original_a_spend = if let [spend] = vec_of_spends.as_slice() { -// spend -// } else { -// panic!("Expected to have one spend here!"); -// }; - -// info!("Verifying the transfers from A -> B wallet..."); -// let cash_notes_for_b: Vec<_> = transfer_to_b.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_b[0]).await?; -// wallet_b.deposit_and_store_to_disk(&cash_notes_for_b)?; // store inside B - -// // Send from B -> C -// let wallet_dir_c = TempDir::new()?; -// let mut wallet_c = get_wallet(wallet_dir_c.path()); -// assert_eq!(wallet_c.balance(), NanoTokens::zero()); - -// let (cash_notes_b, _exclusive_access) = wallet_b.available_cash_notes()?; -// assert!(!cash_notes_b.is_empty()); -// let to_c_unique_key = ( -// wallet_b.balance(), -// wallet_c.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_c = SignedTransaction::new( -// cash_notes_b.clone(), -// vec![to_c_unique_key], -// wallet_b.address(), -// reason.clone(), -// wallet_b.key(), -// )?; - -// client -// .send_spends(transfer_to_c.spends.iter(), false) -// .await?; - -// info!("Verifying the transfers from B -> C wallet..."); -// let cash_notes_for_c: Vec<_> = transfer_to_c.output_cashnotes.clone(); -// client.verify_cashnote(&cash_notes_for_c[0]).await?; -// wallet_c.deposit_and_store_to_disk(&cash_notes_for_c.clone())?; // store inside c - -// // Try to double spend from A -> X -// let wallet_dir_x = TempDir::new()?; -// let wallet_x = get_wallet(wallet_dir_x.path()); -// assert_eq!(wallet_x.balance(), NanoTokens::zero()); - -// let to_x_unique_key = ( -// amount, -// wallet_x.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_x = SignedTransaction::new( -// cash_notes_a.clone(), -// vec![to_x_unique_key], -// wallet_a.address(), -// reason.clone(), -// wallet_a.key(), -// )?; // reuse the old cash notes -// client -// .send_spends(transfer_to_x.spends.iter(), false) -// .await?; -// info!("Verifying the transfers from A -> X wallet... It should error out."); -// let cash_notes_for_x: Vec<_> = transfer_to_x.output_cashnotes.clone(); - -// // sleep for a bit to allow the network to process and accumulate the double spend -// tokio::time::sleep(Duration::from_secs(15)).await; - -// let result = client.verify_cashnote(&cash_notes_for_x[0]).await; -// info!("Got result while verifying double spend from A -> X: {result:?}"); -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); - -// // the original A should still be present as one of the double spends -// let res = client -// .get_spend_from_network(original_a_spend.address()) -// .await; -// assert_matches!( -// res, -// Err(sn_client::Error::Network(NetworkError::DoubleSpendAttempt( -// _ -// ))) -// ); -// if let Err(sn_client::Error::Network(NetworkError::DoubleSpendAttempt(spends))) = res { -// assert!(spends.iter().contains(original_a_spend)) -// } - -// // Try to double spend A -> n different random keys -// for _ in 0..20 { -// info!("Spamming double spends on A"); -// let wallet_dir_y = TempDir::new()?; -// let wallet_y = get_wallet(wallet_dir_y.path()); -// assert_eq!(wallet_y.balance(), NanoTokens::zero()); - -// let to_y_unique_key = ( -// amount, -// wallet_y.address(), -// DerivationIndex::random(&mut rng), -// false, -// ); -// let transfer_to_y = SignedTransaction::new( -// cash_notes_a.clone(), -// vec![to_y_unique_key], -// wallet_a.address(), -// reason.clone(), -// wallet_a.key(), -// )?; // reuse the old cash notes -// client -// .send_spends(transfer_to_y.spends.iter(), false) -// .await?; -// info!("Verifying the transfers from A -> Y wallet... It should error out."); -// let cash_notes_for_y: Vec<_> = transfer_to_y.output_cashnotes.clone(); - -// // sleep for a bit to allow the network to process and accumulate the double spend -// tokio::time::sleep(Duration::from_millis(500)).await; - -// let result = client.verify_cashnote(&cash_notes_for_y[0]).await; -// info!("Got result while verifying double spend from A -> Y: {result:?}"); -// assert_matches!(result, Err(WalletError::CouldNotVerifyTransfer(str)) => { -// assert!(str.starts_with("Network Error Double spend(s) attempt was detected"), "Expected double spend, but got {str}"); -// }); - -// // the original A should still be present as one of the double spends -// let res = client -// .get_spend_from_network(original_a_spend.address()) -// .await; -// assert_matches!( -// res, -// Err(sn_client::Error::Network(NetworkError::DoubleSpendAttempt( -// _ -// ))) -// ); -// if let Err(sn_client::Error::Network(NetworkError::DoubleSpendAttempt(spends))) = res { -// assert!(spends.iter().contains(original_a_spend)) -// } -// } - -// Ok(()) -// } diff --git a/sn_node/tests/sequential_transfers.rs b/sn_node/tests/sequential_transfers.rs deleted file mode 100644 index d6906e37d1..0000000000 --- a/sn_node/tests/sequential_transfers.rs +++ /dev/null @@ -1,54 +0,0 @@ -// // Copyright 2024 MaidSafe.net limited. -// // -// // This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// // Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// // under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// // KIND, either express or implied. Please review the Licences for the specific language governing -// // permissions and limitations relating to use of the SAFE Network Software. - -// mod common; - -// use assert_fs::TempDir; -// use common::client::{get_client_and_funded_wallet, get_wallet}; -// use eyre::Result; -// use sn_client::send; -// use sn_logging::LogBuilder; -// use sn_transfers::NanoTokens; -// use tracing::info; - -// #[tokio::test] -// async fn cash_note_transfer_multiple_sequential_succeed() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("sequential_transfer", true); - -// let first_wallet_dir = TempDir::new()?; - -// let (client, first_wallet) = get_client_and_funded_wallet(first_wallet_dir.path()).await?; -// let first_wallet_balance:NanoTokens = first_wallet.balance(); - -// let second_wallet_balance = first_wallet_balance / 2; -// info!("Transferring from first wallet to second wallet: {second_wallet_balance}."); -// let second_wallet_dir = TempDir::new()?; -// let mut second_wallet = get_wallet(second_wallet_dir.path()); - -// assert_eq!(second_wallet.balance(), NanoTokens::zero()); - -// let tokens = send( -// first_wallet, -// second_wallet_balance, -// second_wallet.address(), -// &client, -// true, -// ) -// .await?; -// info!("Verifying the transfer from first wallet..."); - -// client.verify_cashnote(&tokens).await?; -// second_wallet.deposit_and_store_to_disk(&vec![tokens])?; -// assert_eq!(second_wallet.balance(), second_wallet_balance); -// info!("CashNotes deposited to second wallet: {second_wallet_balance}."); - -// let first_wallet = get_wallet(&first_wallet_dir); -// assert!(second_wallet_balance.as_atto() == first_wallet.balance().as_atto()); - -// Ok(()) -// } diff --git a/sn_node/tests/spend_simulation.rs b/sn_node/tests/spend_simulation.rs deleted file mode 100644 index 3848a344c6..0000000000 --- a/sn_node/tests/spend_simulation.rs +++ /dev/null @@ -1,1162 +0,0 @@ -// // // Copyright 2024 MaidSafe.net limited. -// // // -// // // This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// // // Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// // // under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// // // KIND, either express or implied. Please review the Licences for the specific language governing -// // // permissions and limitations relating to use of the SAFE Network Software. -// -// mod common; -// use assert_fs::TempDir; -// use common::client::{get_client_and_funded_wallet, get_wallet}; -// use eyre::{bail, OptionExt, Report, Result}; -// use itertools::Itertools; -// use rand::{seq::IteratorRandom, Rng}; -// use sn_client::Client; -// use sn_logging::LogBuilder; -// use sn_networking::{GetRecordError, NetworkError}; -// use sn_transfers::{ -// rng, CashNote, DerivationIndex, HotWallet, MainPubkey, NanoTokens, OfflineTransfer, -// SpendAddress, SpendReason, Transaction, UniquePubkey, -// }; -// use std::{ -// collections::{btree_map::Entry, BTreeMap, BTreeSet}, -// fmt::Display, -// path::PathBuf, -// time::Duration, -// }; -// use tokio::sync::mpsc; -// use tracing::*; -// -// const MAX_WALLETS: usize = 15; -// const MAX_CYCLES: usize = 10; -// const AMOUNT_PER_RECIPIENT: NanoTokens = NanoTokens::from(1000); -// /// The chance for an double spend to happen. 1 in X chance. -// const ONE_IN_X_CHANCE_FOR_AN_ATTACK: u32 = 3; -// -// enum WalletAction { -// Send { -// recipients: Vec<(NanoTokens, MainPubkey, DerivationIndex)>, -// }, -// DoubleSpend { -// input_cashnotes_to_double_spend: Vec, -// to: (NanoTokens, MainPubkey, DerivationIndex), -// }, -// ReceiveCashNotes { -// from: WalletId, -// cashnotes: Vec, -// }, -// NotifyAboutInvalidCashNote { -// from: WalletId, -// cashnote: Vec, -// }, -// } -// -// enum WalletTaskResult { -// Error { -// id: WalletId, -// err: String, -// }, -// DoubleSpendSuccess { -// id: WalletId, -// }, -// SendSuccess { -// id: WalletId, -// recipient_cash_notes: Vec, -// change_cash_note: Option, -// transaction: Transaction, -// }, -// ReceiveSuccess { -// id: WalletId, -// received_cash_note: Vec, -// }, -// NotifyAboutInvalidCashNoteSuccess { -// id: WalletId, -// }, -// } -// -// #[derive(Debug)] -// enum SpendStatus { -// Utxo, -// Spent, -// DoubleSpend, -// UtxoWithParentDoubleSpend, -// } -// -// #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -// enum TransactionStatus { -// Valid, -// /// All the inputs have been double spent. -// DoubleSpentInputs, -// } -// -// // Just for printing things -// #[derive(Debug)] -// enum AttackType { -// Poison, -// DoubleSpendAllUxtoOutputs, -// DoubleSpendPartialUtxoOutputs, -// } -// -// // #[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord, Hash)] -// // struct WalletId(usize); -// -// // impl Display for WalletId { -// // fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { -// // write!(f, "WalletId({})", self.0) -// // } -// // } -// -// #[derive(custom_debug::Debug)] -// /// The state of all the wallets and the transactions that they've performed. -// struct State { -// // ========= immutable ========= -// #[debug(skip)] -// /// Sender to send actions to the wallets -// action_senders: BTreeMap>, -// /// The TempDir for each wallet. This has to be held until the end of the test. -// all_wallets: BTreeMap, -// /// The main pubkeys of all the wallets. -// main_pubkeys: BTreeMap, -// /// The map from MainPubKey to WalletId. This is used to get wallets when we only have the cashnote in hand. -// main_pubkeys_inverse: BTreeMap, -// // ========= mutable ========= -// /// The map from UniquePubkey of the cashnote to the actual cashnote and its status. -// cashnote_tracker: BTreeMap, -// /// The map from WalletId to the cashnotes that it has ever received. -// cashnotes_per_wallet: BTreeMap>, -// /// The map from WalletId to the outbound transactions that it has ever sent. -// outbound_transactions_per_wallet: BTreeMap>, -// /// The status of each transaction -// transaction_status: BTreeMap, -// } -// -// #[derive(Debug, Default)] -// struct PendingTasksTracker { -// pending_send_results: Vec, -// pending_notify_invalid_cashnotes_results: Vec, -// pending_receive_results: Vec, -// } -// -// /// This test aims to make sure the PUT validation of nodes are working as expected. We perform valid spends and also -// /// illicit spends and finally verify them to make sure the network processed the spends as expected. -// /// The illicit spends can be of these types: -// /// 1. A double spend of a transaction whose outputs are partially spent / partially UTXO -// /// 2. A double spend of a transcation whose outputs are all UTXO. -// /// 3. Poisoning of a transaction whose outputs are all spent. -// /// Todo: Double spend just 1 input spend. Currently we double spend all the inputs. Have TransactionStatus::DoubleSpentInputs(vec) -// /// -// /// The test works by having a main loop that sends actions to all the wallets. These are then processed by the wallets -// /// in parallel. The wallets send back the results of the actions to the main loop, this is then tracked and the whole -// /// cycle is repeated until the max cycles are reached. -// #[tokio::test] -// async fn spend_simulation() -> Result<()> { -// let _log_guards = LogBuilder::init_single_threaded_tokio_test("spend_simulation", false); -// -// // let (client, mut state) = init_state(MAX_WALLETS).await?; -// -// // let mut rng = rng::thread_rng(); -// // let (result_sender, mut result_rx) = mpsc::channel(10000); -// -// // for (id, wallet_dir) in state.all_wallets.iter() { -// // let (action_sender, action_rx) = mpsc::channel(50); -// // state.action_senders.insert(*id, action_sender); -// // handle_action_per_wallet( -// // *id, -// // wallet_dir.to_path_buf(), -// // client.clone(), -// // action_rx, -// // result_sender.clone(), -// // ); -// // } -// -// // // MAIN LOOP: -// // let mut cycle = 1; -// // while cycle <= MAX_CYCLES { -// // info!("Cycle: {cycle}/{MAX_CYCLES}"); -// // println!("Cycle: {cycle}/{MAX_CYCLES}"); -// // let mut pending_task_results = PendingTasksTracker::default(); -// -// let iter = state -// .action_senders -// .iter() -// .map(|(id, s)| (*id, s.clone())) -// .collect_vec(); -// for (our_id, action_sender) in iter { -// tokio::time::sleep(Duration::from_secs(3)).await; -// let try_performing_illicit_spend = -// rng.gen::() % ONE_IN_X_CHANCE_FOR_AN_ATTACK == 0; -// -// let mut illicit_spend_done = false; -// if try_performing_illicit_spend { -// if let Some(( -// input_cashnotes_to_double_spend, -// output_cashnotes_that_are_unspendable, -// amount, -// attack_type, -// )) = get_cashnotes_to_double_spend(our_id, &mut state)? -// { -// // tell wallets about the cashnotes that will become invalid after we perform the double spend. -// if !output_cashnotes_that_are_unspendable.is_empty() { -// info!("{our_id} is notifying wallets about invalid cashnotes: {output_cashnotes_that_are_unspendable:?}"); -// for (i, sender) in state.action_senders.iter() { -// sender -// .send(WalletAction::NotifyAboutInvalidCashNote { -// from: our_id, -// cashnote: output_cashnotes_that_are_unspendable.clone(), -// }) -// .await?; -// pending_task_results -// .pending_notify_invalid_cashnotes_results -// .push(*i); -// } -// // wait until all the wallets have received the notification. Else we'd try to spend those -// // cashnotes while a double spend has just gone out. -// while !pending_task_results -// .pending_notify_invalid_cashnotes_results -// .is_empty() -// { -// let result = result_rx -// .recv() -// .await -// .ok_or_eyre("Senders will not be dropped")?; -// -// handle_wallet_task_result( -// &mut state, -// result, -// &mut pending_task_results, -// ) -// .await?; -// } -// } -// -// info!( -// "{our_id} is now attempting a {attack_type:?} of {} cashnotes.", -// input_cashnotes_to_double_spend.len() -// ); -// println!( -// "{our_id} is attempting a {attack_type:?} of {} cashnotes", -// input_cashnotes_to_double_spend.len() -// ); -// -// action_sender -// .send(WalletAction::DoubleSpend { -// input_cashnotes_to_double_spend, -// to: ( -// amount, -// state.main_pubkeys[&our_id], -// DerivationIndex::random(&mut rng), -// ), -// }) -// .await?; -// illicit_spend_done = true; -// } -// } -// if !illicit_spend_done { -// let recipients = get_recipients(our_id, &state); -// let recipients_len = recipients.len(); -// action_sender -// .send(WalletAction::Send { -// recipients: recipients -// .into_iter() -// .map(|key| { -// (AMOUNT_PER_RECIPIENT, key, DerivationIndex::random(&mut rng)) -// }) -// .collect_vec(), -// }) -// .await?; -// println!("{our_id} is sending tokens to {recipients_len:?} wallets"); -// } -// -// pending_task_results.pending_send_results.push(our_id); -// if let Ok(result) = result_rx.try_recv() { -// handle_wallet_task_result(&mut state, result, &mut pending_task_results).await?; -// } -// } -// -// // // wait until all send && receive tasks per cycle have been cleared -// // while !pending_task_results.is_empty() { -// // let result = result_rx -// // .recv() -// // .await -// // .ok_or_eyre("Senders will not be dropped")?; -// -// // handle_wallet_task_result(&mut state, result, &mut pending_task_results).await?; -// // } -// -// // // Since it is a tiny network, it will be overwhelemed during the verification of things and will lead to a lot -// // // of Query Timeouts & huge number of pending Get requests. So let them settle. -// // println!("Cycle {cycle} completed. Sleeping for 5s before next cycle."); -// // tokio::time::sleep(Duration::from_secs(5)).await; -// -// // cycle += 1; -// // } -// -// // info!("Final state: {state:?}. Sleeping before verifying wallets."); -// // println!("Verifying all wallets in 10 seconds."); -// // tokio::time::sleep(Duration::from_secs(10)).await; -// // verify_wallets(&state, client).await?; -// -// // Ok(()) -// // } -// -// fn handle_action_per_wallet( -// our_id: WalletId, -// wallet_dir: PathBuf, -// client: Client, -// mut action_rx: mpsc::Receiver, -// result_sender: mpsc::Sender, -// ) { -// tokio::spawn(async move { -// let mut wallet = get_wallet(&wallet_dir); -// let mut invalid_cashnotes = BTreeSet::new(); -// while let Some(action) = action_rx.recv().await { -// let result = inner_handle_action( -// our_id, -// client.clone(), -// action, -// &mut wallet, -// &mut invalid_cashnotes, -// ) -// .await; -// match result { -// Ok(ok) => { -// result_sender.send(ok).await?; -// } -// Err(err) => { -// error!("{our_id} had error handling action : {err}"); -// result_sender -// .send(WalletTaskResult::Error { -// id: our_id, -// err: format!("{err}"), -// }) -// .await?; -// } -// } -// } -// Ok::<_, Report>(()) -// }); -// } -// -// async fn inner_handle_action( -// our_id: WalletId, -// client: Client, -// action: WalletAction, -// wallet: &mut HotWallet, -// invalid_cashnotes: &mut BTreeSet, -// ) -> Result { -// match action { -// WalletAction::Send { recipients } => { -// info!("{our_id} sending to {recipients:?}"); -// let (available_cash_notes, exclusive_access) = wallet.available_cash_notes()?; -// let available_cash_notes = available_cash_notes -// .into_iter() -// .filter(|(note, _)| !invalid_cashnotes.contains(¬e.unique_pubkey())) -// .collect_vec(); -// info!( -// "{our_id} Available CashNotes for local send: {:?}", -// available_cash_notes -// ); -// let transfer = OfflineTransfer::new( -// available_cash_notes, -// recipients, -// wallet.address(), -// SpendReason::default(), -// )?; -// let recipient_cash_notes = transfer.cash_notes_for_recipient.clone(); -// let change = transfer.change_cash_note.clone(); -// let transaction = transfer.build_transaction(); -// -// // wallet.test_update_local_wallet(signed_tx, exclusive_access, true)?; -// -// client -// .send_spends(wallet.unconfirmed_spend_requests().iter(), true) -// .await?; -// wallet.clear_confirmed_spend_requests(); -// if !wallet.unconfirmed_spend_requests().is_empty() { -// bail!("{our_id} has unconfirmed spend requests"); -// } -// -// Ok(WalletTaskResult::SendSuccess { -// id: our_id, -// recipient_cash_notes, -// change_cash_note: change, -// transaction, -// }) -// } -// // todo: we don't track the double spend tx. Track if needed. -// WalletAction::DoubleSpend { -// input_cashnotes_to_double_spend, -// to, -// } => { -// info!( -// "{our_id} double spending cash notes: {:?}", -// input_cashnotes_to_double_spend -// .iter() -// .map(|c| c.unique_pubkey()) -// .collect_vec() -// ); -// let mut input_cashnotes_with_key = -// Vec::with_capacity(input_cashnotes_to_double_spend.len()); -// for cashnote in input_cashnotes_to_double_spend { -// let derived_key = cashnote.derived_key(wallet.key())?; -// input_cashnotes_with_key.push((cashnote, Some(derived_key))); -// } -// let transfer = OfflineTransfer::new( -// input_cashnotes_with_key, -// vec![to], -// wallet.address(), -// SpendReason::default(), -// )?; -// info!("{our_id} double spending transfer: {transfer:?}"); -// -// // client -// // .send_spends(signed_tx.all_spend_requests.iter(), false) -// // .await?; -// -// Ok(WalletTaskResult::DoubleSpendSuccess { id: our_id }) -// } -// WalletAction::ReceiveCashNotes { from, cashnotes } => { -// info!("{our_id} receiving cash note from wallet {from}"); -// wallet.deposit_and_store_to_disk(&cashnotes)?; -// let our_cash_notes = cashnotes -// .into_iter() -// .filter_map(|c| { -// // the same filter used inside the deposit fn -// if c.derived_pubkey(&wallet.address()).is_ok() { -// Some(c) -// } else { -// None -// } -// }) -// .collect::>(); -// Ok(WalletTaskResult::ReceiveSuccess { -// id: our_id, -// received_cash_note: our_cash_notes, -// }) -// } -// WalletAction::NotifyAboutInvalidCashNote { from, cashnote } => { -// info!( -// "{our_id} received notification from {from} about invalid cashnotes: {cashnote:?}. Tracking them" -// ); -// // we're just keeping track of all invalid cashnotes here, not just ours. filtering is a todo, not required for now. -// invalid_cashnotes.extend(cashnote); -// Ok(WalletTaskResult::NotifyAboutInvalidCashNoteSuccess { id: our_id }) -// } -// } -// } -// -// async fn handle_wallet_task_result( -// state: &mut State, -// result: WalletTaskResult, -// pending_task_tracker: &mut PendingTasksTracker, -// ) -> Result<()> { -// match result { -// WalletTaskResult::DoubleSpendSuccess { id } => { -// info!("{id} received a successful double spend result"); -// pending_task_tracker.send_task_completed(id); -// } -// WalletTaskResult::SendSuccess { -// id, -// recipient_cash_notes, -// change_cash_note, -// transaction, -// } => { -// info!( -// "{id} received a successful send result. Tracking the outbound transaction {:?}. Also setting status to TransactionStatus::Valid", -// transaction.hash() -// ); -// pending_task_tracker.send_task_completed(id); -// match state.outbound_transactions_per_wallet.entry(id) { -// Entry::Vacant(entry) => { -// let _ = entry.insert(BTreeSet::from([transaction.clone()])); -// } -// Entry::Occupied(entry) => { -// entry.into_mut().insert(transaction.clone()); -// } -// } -// state -// .transaction_status -// .insert(transaction.clone(), TransactionStatus::Valid); -// -// // mark the input cashnotes as spent -// info!("{id} marking inputs {:?} as spent", transaction.inputs); -// for input in &transaction.inputs { -// // Transaction may contains the `middle payment` -// if let Some((status, _cashnote)) = -// state.cashnote_tracker.get_mut(&input.unique_pubkey) -// { -// *status = SpendStatus::Spent; -// } -// } -// -// // track the change cashnote that is stored by our wallet. -// if let Some(change) = change_cash_note { -// info!( -// "{id} tracking change cash note {} as UTXO", -// change.unique_pubkey() -// ); -// state -// .cashnotes_per_wallet -// .get_mut(&id) -// .ok_or_eyre("Wallet should be present")? -// .push(change.unique_pubkey()); -// let result = state -// .cashnote_tracker -// .insert(change.unique_pubkey(), (SpendStatus::Utxo, change)); -// if result.is_some() { -// bail!("{id} received a new cash note that was already tracked"); -// } -// } -// -// info!("{id}, sending the recipient cash notes to the other wallets"); -// // send the recipient cash notes to the wallets -// for cashnote in recipient_cash_notes { -// let recipient_id = state -// .main_pubkeys_inverse -// .get(cashnote.main_pubkey()) -// .ok_or_eyre("Recipient for cashnote not found")?; -// let sender = state -// .action_senders -// .get(recipient_id) -// .ok_or_eyre("Recipient action sender not found")?; -// sender -// .send(WalletAction::ReceiveCashNotes { -// from: id, -// cashnotes: vec![cashnote], -// }) -// .await?; -// // track the task -// pending_task_tracker -// .pending_receive_results -// .push(*recipient_id); -// } -// } -// WalletTaskResult::ReceiveSuccess { -// id, -// received_cash_note, -// } => { -// info!( -// "{id} received cashnotes successfully. Marking {:?} as UTXO", -// received_cash_note -// .iter() -// .map(|c| c.unique_pubkey()) -// .collect_vec() -// ); -// pending_task_tracker.receive_task_completed(id); -// for cashnote in received_cash_note { -// let unique_pubkey = cashnote.unique_pubkey(); -// let result = state -// .cashnote_tracker -// .insert(unique_pubkey, (SpendStatus::Utxo, cashnote)); -// if result.is_some() { -// bail!("{id} received a new cash note that was already tracked"); -// } -// -// match state.cashnotes_per_wallet.entry(id) { -// Entry::Vacant(_) => { -// bail!("{id} should not be empty, something went wrong.") -// } -// Entry::Occupied(entry) => entry.into_mut().push(unique_pubkey), -// } -// } -// } -// WalletTaskResult::NotifyAboutInvalidCashNoteSuccess { id } => { -// info!("{id} received notification about invalid cashnotes successfully. Marking task as completed."); -// pending_task_tracker.notify_invalid_cashnote_task_completed(id); -// } -// WalletTaskResult::Error { id, err } => { -// error!("{id} had an error: {err}"); -// info!("state: {state:?}"); -// bail!("{id} had an error: {err}"); -// } -// } -// Ok(()) -// } -// -// async fn verify_wallets(state: &State, client: Client) -> Result<()> { -// for (id, spends) in state.cashnotes_per_wallet.iter() { -// println!("Verifying wallet {id}"); -// info!("{id} verifying {} spends", spends.len()); -// let mut wallet = get_wallet(state.all_wallets.get(id).expect("Wallet not found")); -// let (available_cash_notes, _lock) = wallet.available_cash_notes()?; -// for (num, spend) in spends.iter().enumerate() { -// let (status, _cashnote) = state -// .cashnote_tracker -// .get(spend) -// .ok_or_eyre("Something went wrong. Spend not tracked")?; -// info!("{id} verifying status of spend number({num:?}): {spend:?} : {status:?}"); -// match status { -// SpendStatus::Utxo => { -// // TODO: with the new spend struct requiring `middle payment` -// // the transaction no longer covers all spends to be tracked -// // leaving the chance the Spend retain as UTXO even got spent properly -// // Currently just log it, leave for further work of replace transaction -// // with a properly formatted new instance. -// if !available_cash_notes -// .iter() -// .find(|(c, _)| &c.unique_pubkey() == spend) -// .ok_or_eyre("UTXO not found in wallet")?; -// let addr = SpendAddress::from_unique_pubkey(spend); -// let result = client.peek_a_spend(addr).await; -// assert_matches!( -// result, -// Err(sn_client::Error::Network(NetworkError::GetRecordError( -// GetRecordError::RecordNotFound -// ))) -// ); -// } -// SpendStatus::Spent => { -// let addr = SpendAddress::from_unique_pubkey(spend); -// let _spend = client.get_spend_from_network(addr).await?; -// } -// SpendStatus::DoubleSpend => { -// let addr = SpendAddress::from_unique_pubkey(spend); -// match client.get_spend_from_network(addr).await { -// Err(sn_client::Error::Network(NetworkError::DoubleSpendAttempt(_))) => { -// info!("Poisoned spend {addr:?} failed with query attempt"); -// } -// other => { -// warn!("Poisoned spend {addr:?} got unexpected query attempt {other:?}") -// } -// } -// } -// SpendStatus::UtxoWithParentDoubleSpend => { -// // should not have been spent (we're tracking this internally in the test) -// available_cash_notes -// .iter() -// .find(|(c, _)| &c.unique_pubkey() == spend) -// .ok_or_eyre("UTXO not found in wallet")?; -// let addr = SpendAddress::from_unique_pubkey(spend); -// let result = client.peek_a_spend(addr).await; -// assert_matches!( -// result, -// Err(sn_client::Error::Network(NetworkError::GetRecordError( -// GetRecordError::RecordNotFound -// ))) -// ); -// } -// } -// info!("{id} successfully verified spend number({num:?}): {spend:?} : {status:?}"); -// } -// } -// println!("All wallets verified successfully"); -// Ok(()) -// } -// -// /// Create `count` number of wallets and fund them all with equal amounts of tokens. -// /// Return the client and the states of the wallets. -// async fn init_state(count: usize) -> Result<(Client, State)> { -// let mut state = State { -// all_wallets: BTreeMap::new(), -// main_pubkeys: BTreeMap::new(), -// action_senders: BTreeMap::new(), -// main_pubkeys_inverse: BTreeMap::new(), -// cashnote_tracker: BTreeMap::new(), -// cashnotes_per_wallet: BTreeMap::new(), -// outbound_transactions_per_wallet: BTreeMap::new(), -// transaction_status: BTreeMap::new(), -// }; -// -// // for i in 0..count { -// // let wallet_dir = TempDir::new()?; -// // let i = WalletId(i); -// // state -// // .main_pubkeys -// // .insert(i, get_wallet(wallet_dir.path()).address()); -// // state -// // .main_pubkeys_inverse -// // .insert(get_wallet(wallet_dir.path()).address(), i); -// // state.all_wallets.insert(i, wallet_dir); -// // } -// -// // let first_wallet_dir = TempDir::new()?; -// // let (client, mut first_wallet) = get_client_and_funded_wallet(first_wallet_dir.path()).await?; -// -// // let amount = NanoTokens::from(first_wallet.balance().as_nano() / MAX_WALLETS as u64); -// // info!( -// // "Funding all the wallets of len: {} each with {amount} tokens", -// // state.main_pubkeys.len(), -// // ); -// -// // let mut rng = rng::thread_rng(); -// // let reason = SpendReason::default(); -// -// // let mut recipients = Vec::new(); -// // for address in state.main_pubkeys.values() { -// // let to = (amount, *address, DerivationIndex::random(&mut rng)); -// // recipients.push(to); -// // } -// -// // let (available_cash_notes, _lock) = first_wallet.available_cash_notes()?; -// -// // let signed_tx = SignedTransaction::new( -// // available_cash_notes, -// // recipients, -// // first_wallet.address(), -// // reason.clone(), -// // )?; -// -// // info!("Sending signed_tx for all wallets and verifying them"); -// // client -// // .send_spends(signed_tx.all_spend_requests.iter(), true) -// // .await?; -// -// for (id, address) in state.main_pubkeys.iter() { -// let mut wallet = get_wallet(state.all_wallets.get(id).expect("Id should be present")); -// wallet.deposit_and_store_to_disk(&transfer.cash_notes_for_recipient)?; -// trace!( -// "{id} with main_pubkey: {address:?} has balance: {}", -// wallet.balance() -// ); -// assert_eq!(wallet.balance(), amount); -// -// // let (available_cash_notes, _lock) = wallet.available_cash_notes()?; -// -// // for (cashnote, _) in available_cash_notes { -// // state.cashnote_tracker.insert( -// // cashnote.unique_pubkey, -// // (SpendStatus::Utxo, cashnote.clone()), -// // ); -// // match state.cashnotes_per_wallet.entry(*id) { -// // Entry::Vacant(entry) => { -// // let _ = entry.insert(vec![cashnote.unique_pubkey]); -// // } -// // Entry::Occupied(entry) => entry.into_mut().push(cashnote.unique_pubkey), -// // } -// // } -// // } -// -// // Ok((client, state)) -// // } -// -// // /// Returns random recipients to send tokens to. -// // /// Random recipient of random lengths are chosen. -// // fn get_recipients(our_id: WalletId, state: &State) -> Vec<(MainPubkey, WalletId)> { -// // let mut recipients = Vec::new(); -// -// // let mut random_number = our_id; -// // while random_number == our_id { -// // random_number = WalletId(rand::thread_rng().gen_range(0..state.main_pubkeys.len())); -// // } -// // recipients.push((state.main_pubkeys[&random_number], random_number)); -// -// // while random_number.0 % 4 != 0 { -// // random_number = WalletId(rand::thread_rng().gen_range(0..state.main_pubkeys.len())); -// // if random_number != our_id -// // && !recipients -// // .iter() -// // .any(|(_, existing_id)| *existing_id == random_number) -// // { -// // recipients.push((state.main_pubkeys[&random_number], random_number)); -// // } -// // } -// -// info!("{our_id} the recipients for send are: {recipients:?}"); -// recipients -// } -// -// /// Checks our state and tries to perform double spends in these order: -// /// Poison old spend whose outputs are all spent. -// /// Double spend a transaction whose outputs are partially spent / partially UTXO -// /// Double spend a transaction whose outputs are all UTXO. -// /// Returns the set of input cashnotes to double spend and the keys of the output cashnotes that will be unspendable -// /// after the attack. -// #[expect(clippy::type_complexity)] -// fn get_cashnotes_to_double_spend( -// our_id: WalletId, -// state: &mut State, -// ) -> Result, Vec, NanoTokens, AttackType)>> { -// let mut rng = rand::thread_rng(); -// let mut attack_type; -// let mut cashnotes_to_double_spend; -// -// cashnotes_to_double_spend = get_random_transaction_to_poison(our_id, state, &mut rng)?; -// attack_type = AttackType::Poison; -// -// if cashnotes_to_double_spend.is_none() { -// cashnotes_to_double_spend = -// get_random_transaction_with_partially_spent_output(our_id, state, &mut rng)?; -// attack_type = AttackType::DoubleSpendPartialUtxoOutputs; -// } -// if cashnotes_to_double_spend.is_none() { -// cashnotes_to_double_spend = -// get_random_transaction_with_all_unspent_output(our_id, state, &mut rng)?; -// attack_type = AttackType::DoubleSpendAllUxtoOutputs; -// } -// -// if let Some((cashnotes_to_double_spend, output_cash_notes_that_are_unspendable)) = -// cashnotes_to_double_spend -// { -// //gotta make sure the amount adds up to the input, else not all cashnotes will be utilized -// let mut input_total_amount = 0; -// for cashnote in &cashnotes_to_double_spend { -// input_total_amount += cashnote.value()?.as_nano(); -// } -// return Ok(Some(( -// cashnotes_to_double_spend, -// output_cash_notes_that_are_unspendable, -// NanoTokens::from(input_total_amount), -// attack_type, -// ))); -// } -// -// Ok(None) -// } -// -// /// Returns the input cashnotes of a random transaction whose: outputs are all spent. -// /// This also modified the status of the cashnote. -// fn get_random_transaction_to_poison( -// our_id: WalletId, -// state: &mut State, -// rng: &mut rand::rngs::ThreadRng, -// ) -> Result, Vec)>> { -// let Some(our_transactions) = state.outbound_transactions_per_wallet.get(&our_id) else { -// info!("{our_id} has no outbound transactions yet. Skipping double spend"); -// return Ok(None); -// }; -// -// if our_transactions.is_empty() { -// info!("{our_id} has no outbound transactions yet. Skipping double spend"); -// return Ok(None); -// } -// -// // A spend / transaction is poisonable if all of its outputs are already spent. -// let mut poisonable_tx = Vec::new(); -// for tx in our_transactions { -// let tx_status = state -// .transaction_status -// .get(tx) -// .ok_or_eyre("The tx should be present")?; -// // This tx has already been attacked. Skip. -// if tx_status == &TransactionStatus::DoubleSpentInputs { -// continue; -// } -// let mut utxo_found = false; -// for output in &tx.outputs { -// let (status, _) = state -// .cashnote_tracker -// .get(output.unique_pubkey()) -// .ok_or_eyre(format!( -// "Output {} not found in cashnote tracker", -// output.unique_pubkey() -// ))?; -// -// if let SpendStatus::Utxo = *status { -// utxo_found = true; -// break; -// } -// } -// if !utxo_found { -// poisonable_tx.push(tx); -// } -// } -// if !poisonable_tx.is_empty() { -// let random_tx = poisonable_tx -// .into_iter() -// .choose(rng) -// .ok_or_eyre("Cannot choose a random tx")?; -// // update the tx status -// *state -// .transaction_status -// .get_mut(random_tx) -// .ok_or_eyre("The tx should be present")? = TransactionStatus::DoubleSpentInputs; -// -// info!( -// "{our_id} is attempting to double spend a transaction {:?} whose outputs all ALL spent. Setting tx status to TransactionStatus::DoubleSpentInputs", random_tx.hash() -// ); -// info!( -// "{our_id} is marking inputs {:?} as DoubleSpend", -// random_tx -// .inputs -// .iter() -// .map(|i| i.unique_pubkey()) -// .collect_vec() -// ); -// -// let mut cashnotes_to_double_spend = Vec::new(); -// for input in &random_tx.inputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get_mut(&input.unique_pubkey) -// .ok_or_eyre("Input spend not tracked")?; -// *status = SpendStatus::DoubleSpend; -// cashnotes_to_double_spend.push(cashnote.clone()); -// } -// -// return Ok(Some((cashnotes_to_double_spend, vec![]))); -// } -// Ok(None) -// } -// -// /// Returns the input cashnotes of a random transaction whose: outputs are partially spent / partially UTXO. -// /// Also returns the uniquepub key of output UTXOs that will be unspendable after the attack. This info is sent to -// /// each wallet, so that they don't try to spend these outputs. -// /// This also modified the status of the cashnote. -// fn get_random_transaction_with_partially_spent_output( -// our_id: WalletId, -// state: &mut State, -// rng: &mut rand::rngs::ThreadRng, -// ) -> Result, Vec)>> { -// let Some(our_transactions) = state.outbound_transactions_per_wallet.get(&our_id) else { -// info!("{our_id} has no outbound transactions yet. Skipping double spend"); -// return Ok(None); -// }; -// -// if our_transactions.is_empty() { -// info!("{our_id} has no outbound transactions yet. Skipping double spend"); -// return Ok(None); -// } -// -// // The list of transactions that have outputs that are partially spent / partially UTXO. -// let mut double_spendable_tx = Vec::new(); -// for tx in our_transactions { -// let tx_status = state -// .transaction_status -// .get(tx) -// .ok_or_eyre("The tx should be present")?; -// // This tx has already been attacked. Skip. -// if tx_status == &TransactionStatus::DoubleSpentInputs { -// continue; -// } -// let mut utxo_found = false; -// let mut spent_output_found = false; -// let mut change_cashnote_found = false; -// for output in &tx.outputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get(output.unique_pubkey()) -// .ok_or_eyre(format!( -// "Output {} not found in cashnote tracker", -// output.unique_pubkey() -// ))?; -// -// match status { -// SpendStatus::Utxo => { -// // skip if the cashnote is the change. The test can't progress if we make the change unspendable. -// if cashnote.value()? > NanoTokens::from(AMOUNT_PER_RECIPIENT.as_nano()*10) { -// change_cashnote_found = true; -// break; -// } -// utxo_found = true; -// }, -// SpendStatus::UtxoWithParentDoubleSpend => bail!("UtxoWithParentDoubleSpend should not be present here. We skip txs that has been attacked"), -// SpendStatus::Spent -// // DoubleSpend can be present. TransactionStatus::DoubleSpentInputs means that inputs are double spent, we skip those. -// // So the output with DoubleSpend will be present here. -// | SpendStatus::DoubleSpend => spent_output_found = true, -// -// } -// } -// if change_cashnote_found { -// continue; -// } else if utxo_found && spent_output_found { -// double_spendable_tx.push(tx); -// } -// } -// -// if !double_spendable_tx.is_empty() { -// let random_tx = double_spendable_tx -// .into_iter() -// .choose(rng) -// .ok_or_eyre("Cannot choose a random tx")?; -// // update the tx status -// *state -// .transaction_status -// .get_mut(random_tx) -// .ok_or_eyre("The tx should be present")? = TransactionStatus::DoubleSpentInputs; -// -// info!("{our_id} is attempting to double spend a transaction {:?} whose outputs are partially spent. Setting tx status to TransactionStatus::DoubleSpentInputs", random_tx.hash()); -// info!( -// "{our_id} is marking inputs {:?} as DoubleSpend", -// random_tx -// .inputs -// .iter() -// .map(|i| i.unique_pubkey()) -// .collect_vec() -// ); -// -// let mut cashnotes_to_double_spend = Vec::new(); -// for input in &random_tx.inputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get_mut(&input.unique_pubkey) -// .ok_or_eyre("Input spend not tracked")?; -// *status = SpendStatus::DoubleSpend; -// cashnotes_to_double_spend.push(cashnote.clone()); -// } -// -// let mut marked_output_as_cashnotes_unspendable_utxo = Vec::new(); -// for output in &random_tx.outputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get_mut(output.unique_pubkey()) -// .ok_or_eyre("Output spend not tracked")?; -// if let SpendStatus::Utxo = *status { -// *status = SpendStatus::UtxoWithParentDoubleSpend; -// marked_output_as_cashnotes_unspendable_utxo.push(cashnote.unique_pubkey); -// } -// } -// info!( -// "{our_id} is marking some outputs {:?} as UtxoWithParentDoubleSpend", -// marked_output_as_cashnotes_unspendable_utxo -// ); -// -// return Ok(Some(( -// cashnotes_to_double_spend, -// marked_output_as_cashnotes_unspendable_utxo, -// ))); -// } -// -// Ok(None) -// } -// -// /// Returns the input cashnotes of a random transaction whose: outputs are all UTXO. -// /// Also returns the uniquepub key of output UTXOs that will be unspendable after the attack. This info is sent to -// /// each wallet, so that they don't try to spend these outputs. -// /// This also modified the status of the cashnote. -// fn get_random_transaction_with_all_unspent_output( -// our_id: WalletId, -// state: &mut State, -// rng: &mut rand::rngs::ThreadRng, -// ) -> Result, Vec)>> { -// let Some(our_transactions) = state.outbound_transactions_per_wallet.get(&our_id) else { -// info!("{our_id} has no outbound transactions yet. Skipping double spend"); -// return Ok(None); -// }; -// -// if our_transactions.is_empty() { -// info!("{our_id} has no outbound transactions yet. Skipping double spend"); -// return Ok(None); -// } -// -// let mut double_spendable_tx = Vec::new(); -// for tx in our_transactions { -// let tx_status = state -// .transaction_status -// .get(tx) -// .ok_or_eyre("The tx should be present")?; -// if tx_status == &TransactionStatus::DoubleSpentInputs { -// continue; -// } -// let mut all_utxos = true; -// let mut change_cashnote_found = false; -// for output in &tx.outputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get(output.unique_pubkey()) -// .ok_or_eyre(format!( -// "Output {} not found in cashnote tracker", -// output.unique_pubkey() -// ))?; -// -// match status { -// SpendStatus::Utxo => { -// // skip if the cashnote is the change. The test can't progress if we make the change unspendable. -// if cashnote.value()? > NanoTokens::from(AMOUNT_PER_RECIPIENT.as_nano()*10) { -// change_cashnote_found = true; -// break; -// } -// } -// SpendStatus::UtxoWithParentDoubleSpend => bail!("UtxoWithParentDoubleSpend should not be present here. We skip txs that has been attacked"), -// _ => { -// all_utxos = false; -// break; -// } -// } -// } -// if change_cashnote_found { -// continue; -// } else if all_utxos { -// double_spendable_tx.push(tx); -// } -// } -// -// if !double_spendable_tx.is_empty() { -// let random_tx = double_spendable_tx -// .into_iter() -// .choose(rng) -// .ok_or_eyre("Cannot choose a random tx")?; -// // update the tx status -// *state -// .transaction_status -// .get_mut(random_tx) -// .ok_or_eyre("The tx should be present")? = TransactionStatus::DoubleSpentInputs; -// -// info!("{our_id} is attempting to double spend a transaction {:?} whose outputs are all UTXO. Setting tx status to TransactionStatus::DoubleSpentInputs", random_tx.hash()); -// info!( -// "{our_id} is marking inputs {:?} as DoubleSpend", -// random_tx -// .inputs -// .iter() -// .map(|i| i.unique_pubkey()) -// .collect_vec() -// ); -// -// let mut cashnotes_to_double_spend = Vec::new(); -// for input in &random_tx.inputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get_mut(&input.unique_pubkey) -// .ok_or_eyre("Input spend not tracked")?; -// *status = SpendStatus::DoubleSpend; -// cashnotes_to_double_spend.push(cashnote.clone()); -// } -// -// let mut marked_output_cashnotes_as_unspendable_utxo = Vec::new(); -// for output in &random_tx.outputs { -// let (status, cashnote) = state -// .cashnote_tracker -// .get_mut(output.unique_pubkey()) -// .ok_or_eyre("Output spend not tracked")?; -// *status = SpendStatus::UtxoWithParentDoubleSpend; -// marked_output_cashnotes_as_unspendable_utxo.push(cashnote.unique_pubkey); -// } -// info!( -// "{our_id} is marking all outputs {:?} as UtxoWithParentDoubleSpend", -// marked_output_cashnotes_as_unspendable_utxo -// ); -// -// return Ok(Some(( -// cashnotes_to_double_spend, -// marked_output_cashnotes_as_unspendable_utxo, -// ))); -// } -// -// Ok(None) -// } -// -// impl PendingTasksTracker { -// fn is_empty(&self) -> bool { -// self.pending_send_results.is_empty() -// && self.pending_receive_results.is_empty() -// && self.pending_notify_invalid_cashnotes_results.is_empty() -// } -// -// // fn send_task_completed(&mut self, id: WalletId) { -// // let pos = self -// // .pending_send_results -// // .iter() -// // .position(|x| *x == id) -// // .unwrap_or_else(|| panic!("Send task for {id} was not found ")); -// // self.pending_send_results.remove(pos); -// // } -// -// fn receive_task_completed(&mut self, id: WalletId) { -// let pos = self -// .pending_receive_results -// .iter() -// .position(|x| *x == id) -// .unwrap_or_else(|| panic!("Receive task for {id} was not found ")); -// self.pending_receive_results.remove(pos); -// } -// -// fn notify_invalid_cashnote_task_completed(&mut self, id: WalletId) { -// let pos = self -// .pending_notify_invalid_cashnotes_results -// .iter() -// .position(|x| *x == id) -// .unwrap_or_else(|| panic!("Notify invalid cashnote task for {id} was not found ")); -// self.pending_notify_invalid_cashnotes_results.remove(pos); -// } -// } diff --git a/sn_node_manager/Cargo.toml b/sn_node_manager/Cargo.toml index 67070cec2f..865e29d8c7 100644 --- a/sn_node_manager/Cargo.toml +++ b/sn_node_manager/Cargo.toml @@ -53,7 +53,6 @@ sn_protocol = { path = "../sn_protocol", version = "0.17.15" } sn_service_management = { path = "../sn_service_management", version = "0.4.3" } sn-releases = "0.2.6" sn_evm = { path = "../sn_evm", version = "0.1.4" } -sn_transfers = { path = "../sn_transfers", version = "0.20.3" } sysinfo = "0.30.12" thiserror = "1.0.23" tokio = { version = "1.26", features = ["full"] } diff --git a/sn_node_manager/src/cmd/faucet.rs b/sn_node_manager/src/cmd/faucet.rs index 6645d9b6f0..49ba53e039 100644 --- a/sn_node_manager/src/cmd/faucet.rs +++ b/sn_node_manager/src/cmd/faucet.rs @@ -7,6 +7,7 @@ // permissions and limitations relating to use of the SAFE Network Software. use super::{download_and_get_upgrade_bin_path, print_upgrade_summary}; +use crate::helpers::get_faucet_data_dir; use crate::{ add_services::{add_faucet, config::AddFaucetServiceOptions}, config::{self, is_running_as_root}, @@ -22,7 +23,6 @@ use sn_service_management::{ control::{ServiceControl, ServiceController}, FaucetService, NodeRegistry, UpgradeOptions, }; -use sn_transfers::get_faucet_data_dir; use std::path::PathBuf; pub async fn add( diff --git a/sn_node_manager/src/cmd/node.rs b/sn_node_manager/src/cmd/node.rs index 049a1d2337..f435c26801 100644 --- a/sn_node_manager/src/cmd/node.rs +++ b/sn_node_manager/src/cmd/node.rs @@ -31,7 +31,6 @@ use sn_service_management::{ rpc::RpcClient, NodeRegistry, NodeService, ServiceStateActions, ServiceStatus, UpgradeOptions, UpgradeResult, }; -use sn_transfers::HotWallet; use std::{cmp::Ordering, io::Write, net::Ipv4Addr, path::PathBuf, str::FromStr, time::Duration}; use tracing::debug; @@ -211,13 +210,8 @@ pub async fn balance( let node = &mut node_registry.nodes[index]; let rpc_client = RpcClient::from_socket_addr(node.rpc_socket_addr); let service = NodeService::new(node, Box::new(rpc_client)); - let wallet = HotWallet::load_from(&service.service_data.data_dir_path) - .inspect_err(|err| error!("Error while loading hot wallet: {err:?}"))?; - println!( - "{}: {}", - service.service_data.service_name, - wallet.balance() - ); + // TODO: remove this as we have no way to know the reward balance of nodes since EVM payments! + println!("{}: {}", service.service_data.service_name, 0,); } Ok(()) } diff --git a/sn_node_manager/src/helpers.rs b/sn_node_manager/src/helpers.rs index bd0ca2baae..2b3e3b7d1d 100644 --- a/sn_node_manager/src/helpers.rs +++ b/sn_node_manager/src/helpers.rs @@ -25,6 +25,17 @@ use crate::{add_services::config::PortRange, config, VerbosityLevel}; const MAX_DOWNLOAD_RETRIES: u8 = 3; +// We need deterministic and fix path for the faucet wallet. +// Otherwise the test instances will not be able to find the same faucet instance. +pub fn get_faucet_data_dir() -> PathBuf { + let mut data_dirs = dirs_next::data_dir().expect("A homedir to exist."); + data_dirs.push("safe"); + data_dirs.push("test_faucet"); + std::fs::create_dir_all(data_dirs.as_path()) + .expect("Faucet test path to be successfully created."); + data_dirs +} + #[cfg(windows)] pub async fn configure_winsw(dest_path: &Path, verbosity: VerbosityLevel) -> Result<()> { if which::which("winsw.exe").is_ok() { diff --git a/sn_node_manager/src/lib.rs b/sn_node_manager/src/lib.rs index b73ed48612..77bb4ec33d 100644 --- a/sn_node_manager/src/lib.rs +++ b/sn_node_manager/src/lib.rs @@ -41,14 +41,12 @@ impl From for VerbosityLevel { use crate::error::{Error, Result}; use colored::Colorize; use semver::Version; -use sn_evm::AttoTokens; use sn_service_management::rpc::RpcActions; use sn_service_management::{ control::ServiceControl, error::Error as ServiceError, rpc::RpcClient, NodeRegistry, NodeService, NodeServiceData, ServiceStateActions, ServiceStatus, UpgradeOptions, UpgradeResult, }; -use sn_transfers::HotWallet; use tracing::debug; pub const DAEMON_DEFAULT_PORT: u16 = 12500; @@ -549,17 +547,8 @@ pub async fn refresh_node_registry( for node in &mut node_registry.nodes { // The `status` command can run before a node is started and therefore before its wallet // exists. - match HotWallet::try_load_from(&node.data_dir_path) { - Ok(wallet) => { - node.reward_balance = Some(AttoTokens::from_u64(wallet.balance().as_nano())); - trace!( - "Wallet balance for node {}: {}", - node.service_name, - wallet.balance() - ); - } - Err(_) => node.reward_balance = None, - } + // TODO: remove this as we have no way to know the reward balance of nodes since EVM payments! + node.reward_balance = None; let mut rpc_client = RpcClient::from_socket_addr(node.rpc_socket_addr); rpc_client.set_max_attempts(1); diff --git a/sn_node_manager/src/local.rs b/sn_node_manager/src/local.rs index 97d0b9a716..d7553f55e1 100644 --- a/sn_node_manager/src/local.rs +++ b/sn_node_manager/src/local.rs @@ -11,12 +11,12 @@ use crate::helpers::{ check_port_availability, get_bin_version, get_start_port_if_applicable, increment_port_option, }; +#[cfg(feature = "faucet")] +use crate::helpers::get_faucet_data_dir; #[cfg(feature = "faucet")] use crate::helpers::get_username; #[cfg(feature = "faucet")] use sn_service_management::FaucetServiceData; -#[cfg(feature = "faucet")] -use sn_transfers::get_faucet_data_dir; use color_eyre::eyre::OptionExt; use color_eyre::{eyre::eyre, Result}; diff --git a/sn_node_rpc_client/Cargo.toml b/sn_node_rpc_client/Cargo.toml index d7e2448a67..44d042a3b3 100644 --- a/sn_node_rpc_client/Cargo.toml +++ b/sn_node_rpc_client/Cargo.toml @@ -31,7 +31,6 @@ sn_node = { path = "../sn_node", version = "0.112.6" } sn_peers_acquisition = { path = "../sn_peers_acquisition", version = "0.5.7" } sn_protocol = { path = "../sn_protocol", version = "0.17.15", features=["rpc"] } sn_service_management = { path = "../sn_service_management", version = "0.4.3" } -sn_transfers = { path = "../sn_transfers", version = "0.20.3" } thiserror = "1.0.23" # # watch out updating this, protoc compiler needs to be installed on all build systems # # arm builds + musl are very problematic diff --git a/sn_node_rpc_client/src/main.rs b/sn_node_rpc_client/src/main.rs index 7930a3b712..43c661d1ec 100644 --- a/sn_node_rpc_client/src/main.rs +++ b/sn_node_rpc_client/src/main.rs @@ -92,7 +92,6 @@ async fn main() -> Result<()> { // For client, default to log to std::out let logging_targets = vec![ ("safenode".to_string(), Level::INFO), - ("sn_transfers".to_string(), Level::INFO), ("sn_networking".to_string(), Level::INFO), ("sn_node".to_string(), Level::INFO), ]; diff --git a/sn_protocol/Cargo.toml b/sn_protocol/Cargo.toml index d86df46734..a98f72ac4d 100644 --- a/sn_protocol/Cargo.toml +++ b/sn_protocol/Cargo.toml @@ -29,7 +29,6 @@ serde = { version = "1.0.133", features = [ "derive", "rc" ]} serde_json = "1.0" sha2 = "0.10.7" sn_build_info = { path = "../sn_build_info", version = "0.1.19" } -sn_transfers = { path = "../sn_transfers", version = "0.20.3" } sn_registers = { path = "../sn_registers", version = "0.4.3" } sn_evm = { path = "../sn_evm", version = "0.1.4" } thiserror = "1.0.23" diff --git a/sn_protocol/README.md b/sn_protocol/README.md index 03c22c405c..9c51e8cf21 100644 --- a/sn_protocol/README.md +++ b/sn_protocol/README.md @@ -27,10 +27,6 @@ The `error.rs` file contains the definitions for various errors that can occur w - Example: `Result::Err(Error::ChunkNotStored(xor_name))` - `RegisterNotFound(Box)`: Indicates that a register was not found. - Example: `Result::Err(Error::RegisterNotFound(register_address))` -- `SpendNotFound(SpendAddress)`: Indicates that a spend was not found. - - Example: `Result::Err(Error::SpendNotFound(cash_note_address))` -- `DoubleSpendAttempt(Box, Box)`: Indicates a double spend attempt. - - Example: `Result::Err(Error::DoubleSpendAttempt(spend1, spend2))` ## Messages @@ -75,7 +71,7 @@ The `storage` module handles the storage aspects of the protocol. ### API Calls - `ChunkAddress`: Address of a chunk in the network. -- `SpendAddress`: Address of a CashNote's Spend in the network. +- `TransactionAddress`: Address of a CashNote's Spend in the network. - `Header`: Header information for storage items. ## Protobuf Definitions diff --git a/sn_protocol/src/lib.rs b/sn_protocol/src/lib.rs index a9a0b3bbfc..6db02f308d 100644 --- a/sn_protocol/src/lib.rs +++ b/sn_protocol/src/lib.rs @@ -17,7 +17,7 @@ pub mod messages; pub mod node; /// RPC commands to node pub mod node_rpc; -/// Storage types for spends, chunks and registers. +/// Storage types for transactions, chunks and registers. pub mod storage; /// Network versioning pub mod version; @@ -31,7 +31,7 @@ pub mod safenode_proto { pub use error::Error; use storage::ScratchpadAddress; -use self::storage::{ChunkAddress, RegisterAddress, SpendAddress}; +use self::storage::{ChunkAddress, RegisterAddress, TransactionAddress}; /// Re-export of Bytes used throughout the protocol pub use bytes::Bytes; @@ -80,8 +80,8 @@ pub enum NetworkAddress { PeerId(Bytes), /// The NetworkAddress is representing a ChunkAddress. ChunkAddress(ChunkAddress), - /// The NetworkAddress is representing a SpendAddress. - SpendAddress(SpendAddress), + /// The NetworkAddress is representing a TransactionAddress. + TransactionAddress(TransactionAddress), /// The NetworkAddress is representing a ChunkAddress. RegisterAddress(RegisterAddress), /// The NetworkAddress is representing a RecordKey. @@ -96,11 +96,11 @@ impl NetworkAddress { NetworkAddress::ChunkAddress(chunk_address) } - /// Return a `NetworkAddress` representation of the `SpendAddress`. - pub fn from_spend_address(cash_note_address: SpendAddress) -> Self { - NetworkAddress::SpendAddress(cash_note_address) + /// Return a `NetworkAddress` representation of the `TransactionAddress`. + pub fn from_transaction_address(transaction_address: TransactionAddress) -> Self { + NetworkAddress::TransactionAddress(transaction_address) } - /// Return a `NetworkAddress` representation of the `SpendAddress`. + /// Return a `NetworkAddress` representation of the `TransactionAddress`. pub fn from_scratchpad_address(address: ScratchpadAddress) -> Self { NetworkAddress::ScratchpadAddress(address) } @@ -125,8 +125,8 @@ impl NetworkAddress { match self { NetworkAddress::PeerId(bytes) | NetworkAddress::RecordKey(bytes) => bytes.to_vec(), NetworkAddress::ChunkAddress(chunk_address) => chunk_address.xorname().0.to_vec(), - NetworkAddress::SpendAddress(cash_note_address) => { - cash_note_address.xorname().0.to_vec() + NetworkAddress::TransactionAddress(transaction_address) => { + transaction_address.xorname().0.to_vec() } NetworkAddress::ScratchpadAddress(addr) => addr.xorname().0.to_vec(), NetworkAddress::RegisterAddress(register_address) => { @@ -149,7 +149,9 @@ impl NetworkAddress { /// Try to return the represented `XorName`. pub fn as_xorname(&self) -> Option { match self { - NetworkAddress::SpendAddress(cash_note_address) => Some(*cash_note_address.xorname()), + NetworkAddress::TransactionAddress(transaction_address) => { + Some(*transaction_address.xorname()) + } NetworkAddress::ChunkAddress(chunk_address) => Some(*chunk_address.xorname()), NetworkAddress::RegisterAddress(register_address) => Some(register_address.xorname()), NetworkAddress::ScratchpadAddress(address) => Some(address.xorname()), @@ -173,8 +175,8 @@ impl NetworkAddress { NetworkAddress::RegisterAddress(register_address) => { RecordKey::new(®ister_address.xorname()) } - NetworkAddress::SpendAddress(cash_note_address) => { - RecordKey::new(cash_note_address.xorname()) + NetworkAddress::TransactionAddress(transaction_address) => { + RecordKey::new(transaction_address.xorname()) } NetworkAddress::ScratchpadAddress(addr) => RecordKey::new(&addr.xorname()), NetworkAddress::PeerId(bytes) => RecordKey::new(bytes), @@ -223,10 +225,10 @@ impl Debug for NetworkAddress { &chunk_address.to_hex()[0..6] ) } - NetworkAddress::SpendAddress(spend_address) => { + NetworkAddress::TransactionAddress(transaction_address) => { format!( - "NetworkAddress::SpendAddress({} - ", - &spend_address.to_hex()[0..6] + "NetworkAddress::TransactionAddress({} - ", + &transaction_address.to_hex()[0..6] ) } NetworkAddress::ScratchpadAddress(scratchpad_address) => { @@ -261,8 +263,8 @@ impl Display for NetworkAddress { NetworkAddress::ChunkAddress(addr) => { write!(f, "NetworkAddress::ChunkAddress({addr:?})") } - NetworkAddress::SpendAddress(addr) => { - write!(f, "NetworkAddress::SpendAddress({addr:?})") + NetworkAddress::TransactionAddress(addr) => { + write!(f, "NetworkAddress::TransactionAddress({addr:?})") } NetworkAddress::ScratchpadAddress(addr) => { write!(f, "NetworkAddress::ScratchpadAddress({addr:?})") @@ -397,19 +399,19 @@ impl std::fmt::Debug for PrettyPrintRecordKey<'_> { #[cfg(test)] mod tests { + use crate::storage::TransactionAddress; use crate::NetworkAddress; use bls::rand::thread_rng; - use sn_transfers::SpendAddress; #[test] - fn verify_spend_addr_is_actionable() { + fn verify_transaction_addr_is_actionable() { let xorname = xor_name::XorName::random(&mut thread_rng()); - let spend_addr = SpendAddress::new(xorname); - let net_addr = NetworkAddress::from_spend_address(spend_addr); + let transaction_addr = TransactionAddress::new(xorname); + let net_addr = NetworkAddress::from_transaction_address(transaction_addr); - let spend_addr_hex = &spend_addr.to_hex()[0..6]; // we only log the first 6 chars + let transaction_addr_hex = &transaction_addr.to_hex()[0..6]; // we only log the first 6 chars let net_addr_fmt = format!("{net_addr}"); - assert!(net_addr_fmt.contains(spend_addr_hex)); + assert!(net_addr_fmt.contains(transaction_addr_hex)); } } diff --git a/sn_protocol/src/messages/cmd.rs b/sn_protocol/src/messages/cmd.rs index a9618ba3f8..9ebf08c94c 100644 --- a/sn_protocol/src/messages/cmd.rs +++ b/sn_protocol/src/messages/cmd.rs @@ -11,7 +11,7 @@ use crate::{storage::RecordType, NetworkAddress}; use serde::{Deserialize, Serialize}; pub use sn_evm::PaymentQuote; -/// Data and CashNote cmds - recording spends or creating, updating, and removing data. +/// Data and CashNote cmds - recording transactions or creating, updating, and removing data. /// /// See the [`protocol`] module documentation for more details of the types supported by the Safe /// Network, and their semantics. diff --git a/sn_protocol/src/storage.rs b/sn_protocol/src/storage.rs index 38e685f1d7..9d3e675039 100644 --- a/sn_protocol/src/storage.rs +++ b/sn_protocol/src/storage.rs @@ -10,16 +10,18 @@ mod address; mod chunks; mod header; mod scratchpad; +mod transaction; use core::fmt; use exponential_backoff::Backoff; use std::{num::NonZeroUsize, time::Duration}; pub use self::{ - address::{ChunkAddress, RegisterAddress, ScratchpadAddress, SpendAddress}, + address::{ChunkAddress, RegisterAddress, ScratchpadAddress, TransactionAddress}, chunks::Chunk, header::{try_deserialize_record, try_serialize_record, RecordHeader, RecordKind, RecordType}, scratchpad::Scratchpad, + transaction::Transaction, }; /// A strategy that translates into a configuration for exponential backoff. diff --git a/sn_protocol/src/storage/address.rs b/sn_protocol/src/storage/address.rs index a076b97748..06d0bca89f 100644 --- a/sn_protocol/src/storage/address.rs +++ b/sn_protocol/src/storage/address.rs @@ -8,8 +8,9 @@ mod chunk; mod scratchpad; +mod transaction; pub use self::chunk::ChunkAddress; pub use self::scratchpad::ScratchpadAddress; +pub use self::transaction::TransactionAddress; pub use sn_registers::RegisterAddress; -pub use sn_transfers::SpendAddress; diff --git a/sn_protocol/src/storage/address/transaction.rs b/sn_protocol/src/storage/address/transaction.rs new file mode 100644 index 0000000000..399a7a6397 --- /dev/null +++ b/sn_protocol/src/storage/address/transaction.rs @@ -0,0 +1,39 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use bls::PublicKey; +use serde::{Deserialize, Serialize}; +use xor_name::XorName; + +/// Address of a transaction, is derived from the owner's public key +#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] +pub struct TransactionAddress(pub XorName); + +impl TransactionAddress { + pub fn from_owner(owner: PublicKey) -> Self { + Self(XorName::from_content(&owner.to_bytes())) + } + + pub fn new(xor_name: XorName) -> Self { + Self(xor_name) + } + + pub fn xorname(&self) -> &XorName { + &self.0 + } + + pub fn to_hex(&self) -> String { + hex::encode(self.0) + } +} + +impl std::fmt::Debug for TransactionAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "TransactionAddress({})", &self.to_hex()[0..6]) + } +} diff --git a/sn_protocol/src/storage/header.rs b/sn_protocol/src/storage/header.rs index 96a4515526..6ab7a1148f 100644 --- a/sn_protocol/src/storage/header.rs +++ b/sn_protocol/src/storage/header.rs @@ -34,7 +34,7 @@ pub struct RecordHeader { pub enum RecordKind { Chunk, ChunkWithPayment, - Spend, + Transaction, Register, RegisterWithPayment, Scratchpad, @@ -49,7 +49,7 @@ impl Serialize for RecordKind { match *self { Self::ChunkWithPayment => serializer.serialize_u32(0), Self::Chunk => serializer.serialize_u32(1), - Self::Spend => serializer.serialize_u32(2), + Self::Transaction => serializer.serialize_u32(2), Self::Register => serializer.serialize_u32(3), Self::RegisterWithPayment => serializer.serialize_u32(4), Self::Scratchpad => serializer.serialize_u32(5), @@ -67,7 +67,7 @@ impl<'de> Deserialize<'de> for RecordKind { match num { 0 => Ok(Self::ChunkWithPayment), 1 => Ok(Self::Chunk), - 2 => Ok(Self::Spend), + 2 => Ok(Self::Transaction), 3 => Ok(Self::Register), 4 => Ok(Self::RegisterWithPayment), 5 => Ok(Self::Scratchpad), @@ -180,11 +180,11 @@ mod tests { .try_serialize()?; assert_eq!(chunk.len(), RecordHeader::SIZE); - let spend = RecordHeader { - kind: RecordKind::Spend, + let transaction = RecordHeader { + kind: RecordKind::Transaction, } .try_serialize()?; - assert_eq!(spend.len(), RecordHeader::SIZE); + assert_eq!(transaction.len(), RecordHeader::SIZE); let register = RecordHeader { kind: RecordKind::Register, diff --git a/sn_protocol/src/storage/transaction.rs b/sn_protocol/src/storage/transaction.rs new file mode 100644 index 0000000000..4732ef1f2d --- /dev/null +++ b/sn_protocol/src/storage/transaction.rs @@ -0,0 +1,79 @@ +// Copyright 2024 MaidSafe.net limited. +// +// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. +// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed +// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. Please review the Licences for the specific language governing +// permissions and limitations relating to use of the SAFE Network Software. + +use super::address::TransactionAddress; +use serde::{Deserialize, Serialize}; + +// re-exports +pub use bls::{PublicKey, Signature}; + +/// Content of a transaction, limited to 32 bytes +pub type TransactionContent = [u8; 32]; + +/// A generic Transaction on the Network +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, Ord, PartialOrd)] +pub struct Transaction { + pub owner: PublicKey, + pub parent: Vec, + pub content: TransactionContent, + pub outputs: Vec<(PublicKey, TransactionContent)>, + /// signs the above 4 fields with the owners key + pub signature: Signature, +} + +impl Transaction { + pub fn new( + owner: PublicKey, + parent: Vec, + content: TransactionContent, + outputs: Vec<(PublicKey, TransactionContent)>, + signature: Signature, + ) -> Self { + Self { + owner, + parent, + content, + outputs, + signature, + } + } + + pub fn address(&self) -> TransactionAddress { + TransactionAddress::from_owner(self.owner) + } + + pub fn bytes_for_signature(&self) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(&self.owner.to_bytes()); + bytes.extend_from_slice("parent".as_bytes()); + bytes.extend_from_slice( + &self + .parent + .iter() + .map(|p| p.to_bytes()) + .collect::>() + .concat(), + ); + bytes.extend_from_slice("content".as_bytes()); + bytes.extend_from_slice(&self.content); + bytes.extend_from_slice("outputs".as_bytes()); + bytes.extend_from_slice( + &self + .outputs + .iter() + .flat_map(|(p, c)| [&p.to_bytes(), c.as_slice()].concat()) + .collect::>(), + ); + bytes + } + + pub fn verify(&self) -> bool { + self.owner + .verify(&self.signature, self.bytes_for_signature()) + } +} diff --git a/sn_transfers/CHANGELOG.md b/sn_transfers/CHANGELOG.md deleted file mode 100644 index ec4c00a34f..0000000000 --- a/sn_transfers/CHANGELOG.md +++ /dev/null @@ -1,917 +0,0 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -## [0.18.6](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.18.5...sn_transfers-v0.18.6) - 2024-06-04 - -### Other -- release -- release - -## [0.18.5](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.18.4...sn_transfers-v0.18.5) - 2024-06-04 - -### Fixed -- *(transfer)* mismatched key shall result in decryption error - -### Other -- *(transfer)* make discord_name decryption backward compatible - -## [0.18.4](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.18.3...sn_transfers-v0.18.4) - 2024-06-03 - -### Fixed -- enable compile time sk setting for faucet/genesis - -## [0.18.2](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.18.1...sn_transfers-v0.18.2) - 2024-06-03 - -### Added -- *(faucet)* write foundation cash note to disk -- *(keys)* enable compile or runtime override of keys - -### Other -- use secrets during build process - -## [0.18.1](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.18.0...sn_transfers-v0.18.1) - 2024-05-24 - -### Added -- use default keys for genesis, or override -- use different key for payment forward -- remove two uneeded env vars -- pass genesis_cn pub fields separate to hide sk -- hide genesis keypair -- hide genesis keypair -- pass sk_str via cli opt -- *(node)* use separate keys of Foundation and Royalty -- *(wallet)* ensure genesis wallet attempts to load from local on init first -- *(faucet)* make gifting server feat dependent -- tracking beta rewards from the DAG -- *(audit)* collect payment forward statistics -- *(node)* periodically forward reward to specific address -- spend reason enum and sized cipher - -### Fixed -- correct genesis_pk naming -- genesis_cn public fields generated from hard coded value -- invalid spend reason in data payments - -### Other -- *(transfers)* comment and naming updates for clarity -- log genesis PK -- rename improperly named foundation_key -- reconfigure local network owner args -- *(refactor)* stabilise node size to 4k records, -- use const for default user or owner -- resolve errors after reverts -- Revert "feat(node): make spend and cash_note reason field configurable" -- Revert "feat: spend shows the purposes of outputs created for" -- Revert "chore: rename output reason to purpose for clarity" -- Revert "feat(cli): track spend creation reasons during audit" -- Revert "chore: refactor CASH_NOTE_REASON strings to consts" -- Revert "chore: address review comments" -- *(node)* use proper SpendReason enum -- add consts - -## [0.18.0-alpha.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.18.0-alpha.0...sn_transfers-v0.18.0-alpha.1) - 2024-05-07 - -### Added -- *(cli)* track spend creation reasons during audit -- spend shows the purposes of outputs created for -- *(node)* make spend and cash_note reason field configurable -- *(cli)* generate a mnemonic as wallet basis if no wallet found -- *(transfers)* do not genereate wallet by default -- [**breaking**] renamings in CashNote -- [**breaking**] rename token to amount in Spend -- unit testing dag, double spend poisoning tweaks - -### Fixed -- create faucet via account load or generation -- transfer tests for HotWallet creation -- *(client)* move acct_packet mnemonic into client layer -- typo - -### Other -- *(versions)* sync versions with latest crates.io vs -- address review comments -- refactor CASH_NOTE_REASON strings to consts -- rename output reason to purpose for clarity -- addres review comments -- *(transfers)* reduce error size -- *(deps)* bump dependencies -- *(transfer)* unit tests for PaymentQuote -- *(release)* sn_auditor-v0.1.7/sn_client-v0.105.3/sn_networking-v0.14.4/sn_protocol-v0.16.3/sn_build_info-v0.1.7/sn_transfers-v0.17.2/sn_peers_acquisition-v0.2.10/sn_cli-v0.90.4/sn_faucet-v0.4.9/sn_metrics-v0.1.4/sn_node-v0.105.6/sn_service_management-v0.2.4/sn-node-manager-v0.7.4/sn_node_rpc_client-v0.6.8/token_supplies-v0.1.47 -- *(release)* sn_auditor-v0.1.3-alpha.0/sn_client-v0.105.3-alpha.0/sn_networking-v0.14.2-alpha.0/sn_protocol-v0.16.2-alpha.0/sn_build_info-v0.1.7-alpha.0/sn_transfers-v0.17.2-alpha.0/sn_peers_acquisition-v0.2.9-alpha.0/sn_cli-v0.90.3-alpha.0/sn_node-v0.105.4-alpha.0/sn-node-manager-v0.7.3-alpha.0/sn_faucet-v0.4.4-alpha.0/sn_service_management-v0.2.2-alpha.0/sn_node_rpc_client-v0.6.4-alpha.0 - -## [0.17.1](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.17.0...sn_transfers-v0.17.1) - 2024-03-28 - -### Added -- *(transfers)* implement WalletApi to expose common methods - -### Fixed -- *(uploader)* clarify the use of root and wallet dirs - -## [0.17.0](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.16.5...sn_transfers-v0.17.0) - 2024-03-27 - -### Added -- *(faucet)* rate limit based upon wallet locks -- *(transfers)* enable client to check if a quote has expired -- *(transfers)* [**breaking**] support multiple payments for the same xorname -- use Arc inside Client, Network to reduce clone cost - -### Other -- *(node)* refactor pricing metrics - -## [0.16.5](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.16.4...sn_transfers-v0.16.5) - 2024-03-21 - -### Added -- refactor DAG, improve error management and security -- dag error recording - -## [0.16.4](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.16.3...sn_transfers-v0.16.4) - 2024-03-14 - -### Added -- refactor spend validation - -### Other -- improve code quality - -## [0.16.3-alpha.1](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.16.3-alpha.0...sn_transfers-v0.16.3-alpha.1) - 2024-03-08 - -### Added -- [**breaking**] pretty serialisation for unique keys - -## [0.16.2](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.16.1...sn_transfers-v0.16.2) - 2024-03-06 - -### Other -- clean swarm commands errs and spend errors - -## [0.16.1](https://github.com/joshuef/safe_network/compare/sn_transfers-v0.16.0...sn_transfers-v0.16.1) - 2024-03-05 - -### Added -- provide `faucet add` command - -## [0.16.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.9...sn_transfers-v0.16.0) - 2024-02-23 - -### Added -- use the old serialisation as default, add some docs -- warn about old format when detected -- implement backwards compatible deserialisation -- [**breaking**] custom serde for unique keys - -## [0.15.8](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.7...sn_transfers-v0.15.8) - 2024-02-20 - -### Added -- spend and DAG utilities - -## [0.15.7](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.6...sn_transfers-v0.15.7) - 2024-02-20 - -### Added -- *(folders)* move folders/files metadata out of Folders entries - -## [0.15.6](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.5...sn_transfers-v0.15.6) - 2024-02-15 - -### Added -- *(client)* keep payee as part of storage payment cache - -### Other -- minor doc change based on peer review - -## [0.15.5](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.4...sn_transfers-v0.15.5) - 2024-02-14 - -### Other -- *(refactor)* move mod.rs files the modern way - -## [0.15.4](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.3...sn_transfers-v0.15.4) - 2024-02-13 - -### Fixed -- manage the genesis spend case - -## [0.15.3](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.2...sn_transfers-v0.15.3) - 2024-02-08 - -### Other -- copyright update to current year - -## [0.15.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.1...sn_transfers-v0.15.2) - 2024-02-07 - -### Added -- extendable local state DAG in cli - -## [0.15.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.15.0...sn_transfers-v0.15.1) - 2024-02-06 - -### Fixed -- *(node)* derive reward_key from main keypair - -## [0.15.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.43...sn_transfers-v0.15.0) - 2024-02-02 - -### Other -- *(cli)* minor changes to cli comments -- [**breaking**] renaming LocalWallet to HotWallet as it holds the secret key for signing tx -- *(readme)* add instructions of out-of-band transaction signing - -## [0.14.43](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.42...sn_transfers-v0.14.43) - 2024-01-29 - -### Other -- *(sn_transfers)* making some functions/helpers to be constructor methods of public structs - -## [0.14.42](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.41...sn_transfers-v0.14.42) - 2024-01-25 - -### Added -- client webtransport-websys feat - -## [0.14.41](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.40...sn_transfers-v0.14.41) - 2024-01-24 - -### Fixed -- dont lock files with wasm - -### Other -- make tokio dev dep for transfers - -## [0.14.40](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.39...sn_transfers-v0.14.40) - 2024-01-22 - -### Added -- spend dag utils - -## [0.14.39](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.38...sn_transfers-v0.14.39) - 2024-01-18 - -### Added -- *(faucet)* download snapshot of maid balances - -## [0.14.38](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.37...sn_transfers-v0.14.38) - 2024-01-16 - -### Fixed -- *(wallet)* remove unconfirmed_spends file from disk when all confirmed - -## [0.14.37](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.36...sn_transfers-v0.14.37) - 2024-01-15 - -### Fixed -- *(client)* do not store paying-out cash_notes into disk -- *(client)* cache payments via disk instead of memory map - -### Other -- *(client)* collect wallet handling time statistics - -## [0.14.36](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.35...sn_transfers-v0.14.36) - 2024-01-10 - -### Added -- *(transfers)* exposing APIs to build and send cashnotes from transactions signed offline -- *(transfers)* include the derivation index of inputs for generated unsigned transactions -- *(transfers)* exposing an API to create unsigned transfers to be signed offline later on - -### Other -- fixup send_spends and use ExcessiveNanoValue error -- *(transfers)* solving clippy issues about complex fn args - -## [0.14.35](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.34...sn_transfers-v0.14.35) - 2024-01-09 - -### Added -- *(client)* extra sleep between chunk verification - -## [0.14.34](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.33...sn_transfers-v0.14.34) - 2024-01-09 - -### Added -- *(cli)* safe wallet create saves new key - -## [0.14.33](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.32...sn_transfers-v0.14.33) - 2024-01-08 - -### Other -- more doc updates to readme files - -## [0.14.32](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.31...sn_transfers-v0.14.32) - 2024-01-05 - -### Other -- add clippy unwrap lint to workspace - -## [0.14.31](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.30...sn_transfers-v0.14.31) - 2023-12-19 - -### Added -- network royalties through audit POC - -## [0.14.30](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.29...sn_transfers-v0.14.30) - 2023-12-18 - -### Added -- *(transfers)* spent keys and created for others removed -- *(transfers)* add api for cleaning up CashNotes - -## [0.14.29](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.28...sn_transfers-v0.14.29) - 2023-12-14 - -### Other -- *(protocol)* print the first six hex characters for every address type - -## [0.14.28](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.27...sn_transfers-v0.14.28) - 2023-12-12 - -### Added -- *(transfers)* make wallet read resiliant to concurrent writes - -## [0.14.27](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.26...sn_transfers-v0.14.27) - 2023-12-06 - -### Added -- *(wallet)* basic impl of a watch-only wallet API - -### Other -- *(wallet)* adding unit tests for watch-only wallet impl. -- *(wallet)* another refactoring removing more redundant and unused wallet code -- *(wallet)* major refactoring removing redundant and unused code - -## [0.14.26](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.25...sn_transfers-v0.14.26) - 2023-12-06 - -### Other -- remove some needless cloning -- remove needless pass by value -- use inline format args -- add boilerplate for workspace lints - -## [0.14.25](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.24...sn_transfers-v0.14.25) - 2023-12-05 - -### Fixed -- protect against amounts tampering and incomplete spends attack - -## [0.14.24](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.23...sn_transfers-v0.14.24) - 2023-12-05 - -### Other -- *(transfers)* tidier debug methods for Transactions - -## [0.14.23](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.22...sn_transfers-v0.14.23) - 2023-11-29 - -### Added -- verify all the way to genesis -- verify spends through the cli - -### Fixed -- genesis check security flaw - -## [0.14.22](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.21...sn_transfers-v0.14.22) - 2023-11-28 - -### Added -- *(transfers)* serialise wallets and transfers data with MsgPack instead of bincode - -## [0.14.21](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.20...sn_transfers-v0.14.21) - 2023-11-23 - -### Added -- move derivation index random method to itself - -## [0.14.20](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.19...sn_transfers-v0.14.20) - 2023-11-22 - -### Other -- optimise log format of DerivationIndex - -## [0.14.19](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.18...sn_transfers-v0.14.19) - 2023-11-20 - -### Added -- *(networking)* shortcircuit response sending for replication - -## [0.14.18](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.17...sn_transfers-v0.14.18) - 2023-11-20 - -### Added -- quotes - -### Fixed -- use actual quote instead of dummy - -## [0.14.17](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.16...sn_transfers-v0.14.17) - 2023-11-16 - -### Added -- massive cleaning to prepare for quotes - -### Fixed -- wrong royaltie amount -- cashnote mixup when 2 of them are for the same node - -## [0.14.16](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.15...sn_transfers-v0.14.16) - 2023-11-15 - -### Added -- *(royalties)* make royalties payment to be 15% of the total storage cost - -## [0.14.15](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.14...sn_transfers-v0.14.15) - 2023-11-14 - -### Other -- *(royalties)* verify royalties fees amounts - -## [0.14.14](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.13...sn_transfers-v0.14.14) - 2023-11-10 - -### Added -- *(cli)* attempt to reload wallet from disk if storing it fails when receiving transfers online -- *(cli)* new cmd to listen to royalties payments and deposit them into a local wallet - -## [0.14.13](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.12...sn_transfers-v0.14.13) - 2023-11-10 - -### Other -- *(transfers)* more logs around payments... - -## [0.14.12](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.11...sn_transfers-v0.14.12) - 2023-11-09 - -### Other -- simplify when construct payess for storage - -## [0.14.11](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.10...sn_transfers-v0.14.11) - 2023-11-02 - -### Added -- keep transfers in mem instead of heavy cashnotes - -## [0.14.10](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.9...sn_transfers-v0.14.10) - 2023-11-01 - -### Other -- *(node)* don't log the transfers events - -## [0.14.9](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.8...sn_transfers-v0.14.9) - 2023-10-30 - -### Added -- `bincode::serialize` into `Bytes` without intermediate allocation - -## [0.14.8](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.7...sn_transfers-v0.14.8) - 2023-10-27 - -### Added -- *(rpc_client)* show total accumulated balance when decrypting transfers received - -## [0.14.7](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.6...sn_transfers-v0.14.7) - 2023-10-26 - -### Fixed -- typos - -## [0.14.6](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.5...sn_transfers-v0.14.6) - 2023-10-24 - -### Fixed -- *(tests)* nodes rewards tests to account for repayments amounts - -## [0.14.5](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.4...sn_transfers-v0.14.5) - 2023-10-24 - -### Added -- *(payments)* adding unencrypted CashNotes for network royalties and verifying correct payment -- *(payments)* network royalties payment made when storing content - -### Other -- *(api)* wallet APIs to account for network royalties fees when returning total cost paid for storage - -## [0.14.4](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.3...sn_transfers-v0.14.4) - 2023-10-24 - -### Fixed -- *(networking)* only validate _our_ transfers at nodes - -## [0.14.3](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.2...sn_transfers-v0.14.3) - 2023-10-18 - -### Other -- Revert "feat: keep transfers in mem instead of mem and i/o heavy cashnotes" - -## [0.14.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.1...sn_transfers-v0.14.2) - 2023-10-18 - -### Added -- keep transfers in mem instead of mem and i/o heavy cashnotes - -## [0.14.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.14.0...sn_transfers-v0.14.1) - 2023-10-17 - -### Fixed -- *(transfers)* dont overwrite existing payment transactions when we top up - -### Other -- adding comments and cleanup around quorum / payment fixes - -## [0.14.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.12...sn_transfers-v0.14.0) - 2023-10-12 - -### Added -- *(sn_transfers)* dont load Cns from disk, store value along w/ pubkey in wallet -- include protection for deposits - -### Fixed -- remove uneeded hideous key Clone trait -- deadlock -- place lock on another file to prevent windows lock issue -- lock wallet file instead of dir -- wallet concurrent access bugs - -### Other -- more detailed logging when client creating store cash_note - -## [0.13.12](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.11...sn_transfers-v0.13.12) - 2023-10-11 - -### Fixed -- expose RecordMismatch errors and cleanup wallet if we hit that - -### Other -- *(transfers)* add somre more clarity around DoubleSpendAttemptedForCashNotes -- *(docs)* cleanup comments and docs -- *(transfers)* remove pointless api - -## [0.13.11](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.10...sn_transfers-v0.13.11) - 2023-10-10 - -### Added -- *(transfer)* special event for transfer notifs over gossipsub - -## [0.13.10](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.9...sn_transfers-v0.13.10) - 2023-10-10 - -### Other -- *(sn_transfers)* improve transaction build mem perf - -## [0.13.9](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.8...sn_transfers-v0.13.9) - 2023-10-06 - -### Added -- feat!(sn_transfers): unify store api for wallet - -### Fixed -- readd api to load cash_notes from disk, update tests - -### Other -- update comments around RecordNotFound -- remove deposit vs received cashnote disctinction - -## [0.13.8](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.7...sn_transfers-v0.13.8) - 2023-10-06 - -### Other -- fix new clippy errors - -## [0.13.7](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.6...sn_transfers-v0.13.7) - 2023-10-05 - -### Added -- *(metrics)* enable node monitoring through dockerized grafana instance - -## [0.13.6](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.5...sn_transfers-v0.13.6) - 2023-10-05 - -### Fixed -- *(client)* remove concurrency limitations - -## [0.13.5](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.4...sn_transfers-v0.13.5) - 2023-10-05 - -### Fixed -- *(sn_transfers)* be sure we store CashNotes before writing the wallet file - -## [0.13.4](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.3...sn_transfers-v0.13.4) - 2023-10-05 - -### Added -- use progress bars on `files upload` - -## [0.13.3](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.2...sn_transfers-v0.13.3) - 2023-10-04 - -### Added -- *(sn_transfers)* impl From for NanoTokens - -### Fixed -- *(sn_transfers)* reuse payment overflow fix - -### Other -- *(sn_transfers)* clippy and fmt -- *(sn_transfers)* add reuse cashnote cases -- separate method and write test - -## [0.13.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.1...sn_transfers-v0.13.2) - 2023-10-02 - -### Added -- remove unused fee output - -## [0.13.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.13.0...sn_transfers-v0.13.1) - 2023-09-28 - -### Added -- client to client transfers - -## [0.13.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.12.2...sn_transfers-v0.13.0) - 2023-09-27 - -### Added -- deep clean sn_transfers, reduce exposition, remove dead code - -### Fixed -- benches -- uncomment benches in Cargo.toml - -### Other -- optimise bench -- improve cloning -- udeps - -## [0.12.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.12.1...sn_transfers-v0.12.2) - 2023-09-25 - -### Other -- *(transfers)* unused variable removal - -## [0.12.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.12.0...sn_transfers-v0.12.1) - 2023-09-25 - -### Other -- udeps -- cleanup renamings in sn_transfers -- remove mostly outdated mocks - -## [0.12.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.15...sn_transfers-v0.12.0) - 2023-09-21 - -### Added -- rename utxo by CashNoteRedemption -- dusking DBCs - -### Fixed -- udeps -- incompatible hardcoded value, add logs - -### Other -- remove dbc dust comments -- rename Nano NanoTokens -- improve naming - -## [0.11.15](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.14...sn_transfers-v0.11.15) - 2023-09-20 - -### Other -- major dep updates - -## [0.11.14](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.13...sn_transfers-v0.11.14) - 2023-09-18 - -### Added -- serialisation for transfers for out of band sending -- generic transfer receipt - -### Other -- add more docs -- add some docs - -## [0.11.13](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.12...sn_transfers-v0.11.13) - 2023-09-15 - -### Other -- refine log levels - -## [0.11.12](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.11...sn_transfers-v0.11.12) - 2023-09-14 - -### Other -- updated the following local packages: sn_protocol - -## [0.11.11](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.10...sn_transfers-v0.11.11) - 2023-09-13 - -### Added -- *(register)* paying nodes for Register storage - -## [0.11.10](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.9...sn_transfers-v0.11.10) - 2023-09-12 - -### Added -- add tx and parent spends verification -- chunk payments using UTXOs instead of DBCs - -### Other -- use updated sn_dbc - -## [0.11.9](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.8...sn_transfers-v0.11.9) - 2023-09-11 - -### Other -- *(release)* sn_cli-v0.81.29/sn_client-v0.88.16/sn_registers-v0.2.6/sn_node-v0.89.29/sn_testnet-v0.2.120/sn_protocol-v0.6.6 - -## [0.11.8](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.7...sn_transfers-v0.11.8) - 2023-09-08 - -### Added -- *(client)* repay for chunks if they cannot be validated - -## [0.11.7](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.6...sn_transfers-v0.11.7) - 2023-09-05 - -### Other -- *(release)* sn_cli-v0.81.21/sn_client-v0.88.11/sn_registers-v0.2.5/sn_node-v0.89.21/sn_testnet-v0.2.112/sn_protocol-v0.6.5 - -## [0.11.6](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.5...sn_transfers-v0.11.6) - 2023-09-04 - -### Other -- updated the following local packages: sn_protocol - -## [0.11.5](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.4...sn_transfers-v0.11.5) - 2023-09-04 - -### Other -- updated the following local packages: sn_protocol - -## [0.11.4](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.3...sn_transfers-v0.11.4) - 2023-09-01 - -### Other -- *(transfers)* batch dbc storage -- *(transfers)* store dbcs by ref to avoid more clones -- *(transfers)* dont pass by value, this is a clone! -- *(client)* make unconfonfirmed txs btreeset, remove unnecessary cloning -- *(transfers)* improve update_local_wallet - -## [0.11.3](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.2...sn_transfers-v0.11.3) - 2023-08-31 - -### Other -- remove unused async - -## [0.11.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.1...sn_transfers-v0.11.2) - 2023-08-31 - -### Added -- *(node)* node to store rewards in a local wallet - -### Fixed -- *(cli)* don't try to create wallet paths when checking balance - -## [0.11.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.11.0...sn_transfers-v0.11.1) - 2023-08-31 - -### Other -- updated the following local packages: sn_protocol - -## [0.11.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.28...sn_transfers-v0.11.0) - 2023-08-30 - -### Added -- one transfer per data set, mapped dbcs to content addrs -- [**breaking**] pay each chunk holder direct -- feat!(protocol): gets keys with GetStoreCost -- feat!(protocol): get price and pay for each chunk individually -- feat!(protocol): remove chunk merkletree to simplify payment - -### Fixed -- *(tokio)* remove tokio fs - -### Other -- *(deps)* bump tokio to 1.32.0 -- *(client)* refactor client wallet to reduce dbc clones -- *(client)* pass around content payments map mut ref -- *(client)* error out early for invalid transfers - -## [0.10.28](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.27...sn_transfers-v0.10.28) - 2023-08-24 - -### Other -- rust 1.72.0 fixes - -## [0.10.27](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.26...sn_transfers-v0.10.27) - 2023-08-18 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.26](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.25...sn_transfers-v0.10.26) - 2023-08-11 - -### Added -- *(transfers)* add resend loop for unconfirmed txs - -## [0.10.25](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.24...sn_transfers-v0.10.25) - 2023-08-10 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.24](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.23...sn_transfers-v0.10.24) - 2023-08-08 - -### Added -- *(transfers)* add get largest dbc for spending - -### Fixed -- *(node)* prevent panic in storage calcs - -### Other -- *(faucet)* provide more money -- tidy store cost code - -## [0.10.23](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.22...sn_transfers-v0.10.23) - 2023-08-07 - -### Other -- rename network addresses confusing name method to xorname - -## [0.10.22](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.21...sn_transfers-v0.10.22) - 2023-08-01 - -### Other -- *(networking)* use TOTAL_SUPPLY from sn_transfers - -## [0.10.21](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.20...sn_transfers-v0.10.21) - 2023-08-01 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.20](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.19...sn_transfers-v0.10.20) - 2023-08-01 - -### Other -- *(release)* sn_cli-v0.80.17/sn_client-v0.87.0/sn_registers-v0.2.0/sn_node-v0.88.6/sn_testnet-v0.2.44/sn_protocol-v0.4.2 - -## [0.10.19](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.18...sn_transfers-v0.10.19) - 2023-07-31 - -### Fixed -- *(test)* using proper wallets during data_with_churn test - -## [0.10.18](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.17...sn_transfers-v0.10.18) - 2023-07-28 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.17](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.16...sn_transfers-v0.10.17) - 2023-07-26 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.16](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.15...sn_transfers-v0.10.16) - 2023-07-25 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.15](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.14...sn_transfers-v0.10.15) - 2023-07-21 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.14](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.13...sn_transfers-v0.10.14) - 2023-07-20 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.13](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.12...sn_transfers-v0.10.13) - 2023-07-19 - -### Added -- *(CI)* dbc verfication during network churning test - -## [0.10.12](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.11...sn_transfers-v0.10.12) - 2023-07-19 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.11](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.10...sn_transfers-v0.10.11) - 2023-07-18 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.10](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.9...sn_transfers-v0.10.10) - 2023-07-17 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.9](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.8...sn_transfers-v0.10.9) - 2023-07-17 - -### Added -- *(client)* keep storage payment proofs in local wallet - -## [0.10.8](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.7...sn_transfers-v0.10.8) - 2023-07-12 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.7](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.6...sn_transfers-v0.10.7) - 2023-07-11 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.6](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.5...sn_transfers-v0.10.6) - 2023-07-10 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.5](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.4...sn_transfers-v0.10.5) - 2023-07-06 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.4](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.3...sn_transfers-v0.10.4) - 2023-07-05 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.3](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.2...sn_transfers-v0.10.3) - 2023-07-04 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.1...sn_transfers-v0.10.2) - 2023-06-28 - -### Other -- updated the following local packages: sn_protocol - -## [0.10.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.10.0...sn_transfers-v0.10.1) - 2023-06-26 - -### Added -- display path when no deposits were found upon wallet deposit failure - -### Other -- adding proptests for payment proofs merkletree utilities -- payment proof map to use xorname as index instead of merkletree nodes type -- having the payment proof validation util to return the item's leaf index - -## [0.10.0](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.8...sn_transfers-v0.10.0) - 2023-06-22 - -### Added -- use standarised directories for files/wallet commands - -## [0.9.8](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.7...sn_transfers-v0.9.8) - 2023-06-21 - -### Other -- updated the following local packages: sn_protocol - -## [0.9.7](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.6...sn_transfers-v0.9.7) - 2023-06-21 - -### Fixed -- *(sn_transfers)* hardcode new genesis DBC for tests - -### Other -- *(node)* obtain parent_tx from SignedSpend - -## [0.9.6](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.5...sn_transfers-v0.9.6) - 2023-06-20 - -### Other -- updated the following local packages: sn_protocol - -## [0.9.5](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.4...sn_transfers-v0.9.5) - 2023-06-20 - -### Other -- specific error types for different payment proof verification scenarios - -## [0.9.4](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.3...sn_transfers-v0.9.4) - 2023-06-15 - -### Added -- add double spend test - -### Fixed -- parent spend checks -- parent spend issue - -## [0.9.3](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.2...sn_transfers-v0.9.3) - 2023-06-14 - -### Added -- include output DBC within payment proof for Chunks storage - -## [0.9.2](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.1...sn_transfers-v0.9.2) - 2023-06-12 - -### Added -- remove spendbook rw locks, improve logging - -## [0.9.1](https://github.com/maidsafe/safe_network/compare/sn_transfers-v0.9.0...sn_transfers-v0.9.1) - 2023-06-09 - -### Other -- manually change crate version diff --git a/sn_transfers/Cargo.toml b/sn_transfers/Cargo.toml deleted file mode 100644 index 9ca82245af..0000000000 --- a/sn_transfers/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -authors = ["MaidSafe Developers "] -description = "Safe Network Transfer Logic" -documentation = "https://docs.rs/sn_node" -edition = "2021" -homepage = "https://maidsafe.net" -license = "GPL-3.0" -name = "sn_transfers" -readme = "README.md" -repository = "https://github.com/maidsafe/safe_network" -version = "0.20.3" - -[features] -reward-forward = [] -test-utils = [] - -[dependencies] -bls = { package = "blsttc", version = "8.0.1" } -chrono = "0.4.38" -custom_debug = "~0.6.1" -dirs-next = "~2.0.0" -hex = "~0.4.3" -lazy_static = "~1.4.0" -libp2p = { git = "https://github.com/maqi/rust-libp2p.git", branch = "kad_0.46.2", features = ["identify", "kad"] } -rand = { version = "~0.8.5", features = ["small_rng"] } -rmp-serde = "1.1.1" -secrecy = "0.8.0" -serde_bytes = "0.11" -serde = { version = "1.0.133", features = ["derive", "rc"] } -serde_json = "1.0.108" -thiserror = "1.0.24" -tiny-keccak = { version = "~2.0.2", features = ["sha3"] } -tracing = { version = "~0.1.26" } -walkdir = "~2.5.0" -xor_name = "5.0.0" -rayon = "1.8.0" -ring = "0.17.8" -tempfile = "3.10.1" - -[dev-dependencies] -tokio = { version = "1.32.0", features = ["macros", "rt"] } -criterion = "0.5.1" -assert_fs = "1.0.0" -eyre = "0.6.8" - - -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -fs2 = "0.4.3" - -[target."cfg(unix)".dev-dependencies.pprof] -version = "0.13.0" -features = ["flamegraph"] - -[[bench]] -name = "reissue" -harness = false - -[lints] -workspace = true diff --git a/sn_transfers/README.md b/sn_transfers/README.md deleted file mode 100644 index b042367e90..0000000000 --- a/sn_transfers/README.md +++ /dev/null @@ -1,317 +0,0 @@ -# Autonomi Network Token - -The Autonomi Network Token (ANT) is a currency built on top of the storage layer of the Autonomi Network. It is used to reward Network nodes for storing data. -. ANT does not use a blockchain but a distributed Directed Acyclic Graph (DAG) of `Spend`s which are all linked together all the way to the first `Spend` which we call `Genesis`. Those `Spend`s contain transaction data and all the information necessary for verification and audit of the currency. - -## Keys - -Just like many digital currencies, we use [public/private key cryptography](https://en.wikipedia.org/wiki/Public-key_cryptography) (in our case we use [bls](https://en.wikipedia.org/wiki/BLS_digital_signature) keys, implemented in the [blsttc rust crate](https://docs.rs/blsttc/latest/blsttc/)). A wallet consists of two keys: - -- `MainPubkey`: equivalent to a Bitcoin address, this is used to receive ANT. It can be shared publicly. -- `MainSecretKey`: the secret from which a `MainPubkey` is generated; it is used for spending ANT. - -Unlike one might expect, the `MainPubkey` itself never owns any money: `UniquePubkey`s derived from it do. Value is owned by those `UniquePubkey`s which are spendable only once in the form of a `Spend` uploaded at that `UniquePubkey`'s address (known as a `SpendAddress`) on the Network. - -The way we obtain those `UniquePubkey`s is by using bls key derivation, an algorithm which creates a new key from another key by using a large number called a `DerivationIndex`. `UniquePubkey`s are derived from the `MainPubkey`. To spend the value owned by a `UniquePubkey`, one uses the associated `DerivedSecretKey` which was derived from the `MainSecretKey` using the same `DerivationIndex` as was used to create the `UniquePubkey`. - -This `DerivedSecretKey` is used to sign the `Spend` which is then sent to the Network for validation and storage. Once the Network has stored and properly replicated that `Spend`, that `UniquePubkey` is considered to be spent and cannot ever be spent again. If more than one `Spend` entry exist at a given `SpendAddress` on the Network, that key is considered to be burnt which makes any `Spend` refering to it unspendable. - -Without the `DerivationIndex`, there is no way to link a `MainPubkey` to a `UniquePubkey`. Since `UniquePubkey`s are spendable only once, this means every transaction involves new and unique keys which are all unrelated and unlinkable to their original owner's `MainPubkey`. - -Under the hood, those types are simply: - -- `MainPubkey` => `blsttc::PublicKey` -- `UniquePubkey` => `blsttc::PublicKey` (derived from `MainPubkey`) -- `MainSecretKey` => `blsttc::SecretKey` -- `DerivedSecretKey` => `blsttc::SecretKey` (derived from `MainSecretKey`) -- `DerivationIndex` => `u256` (big number impossible to guess, used to derive keys) - - -## Spends - -When a `UniquePubkey` is spent, the owner creates a `Spend` and signs it with the associated `DerivedSecretKey` before uploading it to the Network. A `Spend` contains the following information: - -```rust -pub struct Spend { - pub unique_pubkey: UniquePubkey, - pub ancestors: BTreeSet, - pub descendants: BTreeMap, -} -``` - -A `Spend` refers to -- its own `UniquePubkey` -- its `ancestors` (which refer to it as a one of the `descendants`) -- its `descendants` (which could refer to it as one of the `ancestors`) - -> Note that `ancestors` and `descendants` should not be confused with inputs and outputs of a transaction. If we were to put that in traditional input output terms: -> - The `ancestors` are the inputs of the transaction where `unique_pubkey` is an output. -> - The `unique_pubkey` is an input of the transaction where `descendants` are an output. - -```go - GenesisSpend - / \ - SpendA SpendB - / \ \ - SpendC SpendD SpendE - / \ \ -... ... ... -``` - -> All the `Spend`s on a Network come from Genesis. - -Each descendant is given some of the value of the spent `UniquePubkey`. The value of a `Spend` is the sum of the values inherited from its ancestors. - -```go - SpendS(19) value - / | \ | - 9 4 6 value inherited - / | \ | - SpendW(9) SpendX(4) SpendY(6) value - / \ | | - 6 3 4 value inherited - / \ | | -SpendQ(6) SpendZ(7) V - -``` - -> In the above example, Spend Z has 2 ancestors W and X which gave it respectively `3` and `4`. -> Z's value is the sum of the inherited value from its ancestors: `3 + 4 = 7`. -> -> In this example `SpendW` of value `9` would look something like: -> ``` -> Spend { -> unique_pubkey = W, -> ancestors = {S}, -> descendants = {Z : 3, Q : 6}, -> } -> ``` - -`Spend`s on the Network are always signed by their owner (`DerivedSecretKey`) and come with that signature: - -```rust -pub struct SignedSpend { - pub spend: Spend, - pub derived_key_sig: Signature, -} -``` - -In order to be valid and accepted by the Network a Spend must: -- be addressed at the `SpendAddress` derived from its `UniquePubkey` -- refer to existing and valid ancestors that refer to it as a descendant -- refer to descendants and donate a non zero amount to them -- the sum of the donated value to descendants must be equal to the sum of the Spend's inherited value from its ancestors -- the ancestors must not be burnt - -> If multiple valid spend entries are found at a single address, that `UniquePubkey` is said to be burnt and its descendants will therefore fail the above verification -> ```go -> SpendA -> / \ -> SpendB (SpendD, SpendD) -> / \ \ -> ... [E] [F] -> ``` -> In the figure above, there are two `Spend` entries in the Network for the `UniquePubkey` `D`. We say that `D` is burnt. The result is that `E` and `F` have a burnt parent making them unspendable. -> When fetching `D`, one would get a burnt spend entry as we have two `Spend`s on the Network at that `SpendAddress`: -> ``` -> Spend { -> unique_pubkey = D, -> ancestors = {A}, -> descendants = {E : 3}, -> } -> Spend { -> unique_pubkey = D, -> ancestors = {A}, -> descendants = {F : 3}, -> } -> ``` - -`Spend`s are the only currency related data on the Network, they are stored in a sharded manner by nodes whose address is close to the `UniquePubkey`. This ensures that any other `Spend` with the same `UniquePubkey` is the responsibility of the same nodes, countering knowledge forks. - - -## Spend DAG - -All the spends on the Network form a DAG of `Spend`s, with each `Spend` stored in different locations on the Network. No single node has the entire knowledge of the DAG, but the Network as a whole contains that DAG. - -The Spend DAG starts from Genesis, and by following its descendants recursively, one can find all the `Spend`s on the Network. - -An application collecting all those spends from Genesis could rebuild the DAG locally and use it for auditing or external verification. There is no need to run a node to download the entire DAG as the `Spend`s can be fetched for free by a Network client. Similarly to how blockchains have block explorers, a DAG explorer could be built using this. - -The figure below is an example output of such a DAG collecting application: - -![](./dag.svg) - - -## Transfers - -To perform a `Transfer`, one must have money to spend: own at least a spendable `UniquePubkey` and the key to spend it: -- either the `UniquePubkey`'s secret `DerivationIndex` and the `MainSecretKey` in order to derive the `DerivedSecretKey` -- or just the `DerivedSecretKey`s that owns that `UniquePubkey` - -The `Transfer` needs an amount and a recipient: a `MainPubkey`. All the amounts on the Network are in `NanoTokens`, the smallest unit of ANT (10^-9 ANT). Think of it as the ANT equivalent to Satoshi for Bitcoin or Wei for Ethereum. - -> The following concepts are used in the performing of a transfer: -> - `UniquePubkey`: a unique key that can own money but only be spent once -> - `Spend`: the spend commitment of a `UniquePubkey`, once uploaded to the Network, that key is considered to be spent, if a key is spent more than once, it is considered to be burnt and its descendants unspendable -> - `CashNote`: a package of information associated with a `UniquePubkey`: simplifies the process of creating a `Spend` from it -> - `CashNoteRemption`: the minimal information necessary for a recipient to identify a received `UniquePubkey` and be able to spend it -> - `Transfer`: an encrypted package of `CashNoteRemption`, destined to the recipient - -A Transfer consists of the following steps: - -#### Preparation - -First we need to decide on the transfer's recipient and amount: - -- decide on a recipient: `MainPubkey` and an amount in `NanoTokens` - -Then we gather our local spendable `UniquePubkey`s: - -- gather spendable `UniquePubkey`s we own that make up that amount or more -- gather the ancestors of our `UniquePubkey`s as we need them in the `Spend` - -> All the information regarding a spendable `UniquePubkey` (except for the secret keys) can conveniently be packed together into what we call a `CashNote`: -> ```rust -> pub struct CashNote { -> pub main_pubkey: MainPubkey, -> pub derivation_index: DerivationIndex, -> // note that MainPubkey + DerivationIndex => UniquePubkey -> pub parent_spends: BTreeSet, -> } -> ``` - -Then, to protect the identity of the recipient on the Network, we derive a completely new `UniquePubkey` from the recipient's `MainPubkey` using a randomly generated `DerivationIndex`. From an third party's eye, that `UniquePubkey` is unlinkable to the `MainPubkey` we're sending money to. The result is that only the sender and the recipient know that they are involved in this transfer. - -- creation of `UniquePubkey`(s) for the recipient by deriving them from the recipient's `MainPubkey` with randomly generated `DerivationIndex`(es) - -With all the above data, we can finally create the `Spend`s which represent the sender's commitment to do the transfer. - -- creation of the `Spend`s for each spent `UniquePubkey` - - `unique_pubkey`: `UniquePubkey` we own that we wish to spend - - `ancestors`: reference to the ancestors of that `UniquePubkey` to prove its validity - - `descendants`: reference to the `UniquePubkey`(s) of the recipient(s) - -> Note that the `Spend` does not contain any `DerivationIndex`es nor does it contain any `MainPubkey`s. This makes `Spend`s unlinkable to any of the involved parties. - -```go -// we own: --> UniquePubkey_A of value (4) --> UniquePubkey_B of value (5) -// we send to: --> NewUniquePubkey = RecipientMainPubkey.derive(RandomDerivationIndex) -``` - -#### Commitment - -- sign each `Spend` with the `DerivedSecretKey` that we derive from `MainSecretKey` with that `Spend`'s `UniquePubkey`'s `DerivationIndex` -- upload of the `SignedSpend`s to the Network - -> After this step, it is not possible to cancel the transfer. - -```go - ParentSpendA(4) ParentSpendB(5) <- spends on the Network - \ / - 4 5 - \ / - NewUniquePubkey(9) <- refering to this yet unspent key -``` - -#### Out of Band Transfer - -At this point, the recipient doesn't yet know of: -- the `Spend`(s) we uploaded to the Network for them at `SpendAddress` -- the `UniquePubkey`(s) we created for them which can be obtained from the `DerivationIndex` - -> Note that `SpendAddress`: the network address of a `Spend` is derived from the hash of a `UniquePubkey` - -We send this information out of band in the form of an encrypted `Transfer` encrypted to the recipient's `MainPubkey` so only they can decypher it. - -> Since the `Transfer` is encrypted, it can be sent safely by any chosen media to the recipient: by email, chat app or even shared publicly on a forum. -> -> If the encryption is ever broken, this information is unusable without the recipient's `MainSecretKey`. However, coupled with the recipient's `MainPubkey`, this information can identify the corresponding `UniquePubkey`s that were received in this `Transfer`. - -An encrypted `Transfer` is a list of `CashNoteRedemption`s, each corresponding to one of the received `UniquePubkey`s: - -```rust -pub struct CashNoteRedemption { - pub derivation_index: DerivationIndex, - pub parent_spends: BTreeSet, -} -``` - -It contains the `DerivationIndex` used to derive: -- the `UniquePubkey` that we're receiving from our `MainPubkey` -- the `DerivedSecretKey` from our `MainSecretKey`: needed to spend this new `UniquePubkey` - -#### Redemption and Verification - -Once received and decrypted by the recipient, the `CashNoteRedemption` can be used to verify the transfer using the `Spend`s online and add the received `UniquePubkey`s to our spendable `UniquePubkey`s stash: - -- getting the `UniquePubkey` from the `CashNoteRedemption`'s `DerivationIndex` and our `MainPubkey` -- getting the `Spend`s at the `SpendAddress` on the Network provided in the `CashNoteRedemption` and making sure they all exist on the Network -- verifying the content of those parent `Spend`s - - make sure they all refer to our `UniquePubkey` as a descendant - - make sure they are valid `Spend`s -- the `UniquePubkey` is now ours and spendable! -- for convenience, one can create a `CashNote` with all the above information to simplify spending the received `UniquePubkey` - -> Since `CashNote`s contain sensitive information, they should never be shared or leaked as it would reveal the link between the `MainPubkey` and the `UniquePubkey` of this `CashNote` - -Once successfully received, for safety, it is advised to re-send the received tokens to ourselves on a new `UniquePubkey` that only we can link back to our `MainPubkey`. This ensures: -- that the original sender doesn't have the `DerivationIndex` for our spendable money -- that we know the parent of our spendable `UniquePubkey`s are not burnable by anyone but ourselves - -> Failing to do so exposes the receiver to the risk of having their keys become unspendable if the sender decides to burn the parent `Spend`s - -```go - ParentSpendA(4) ParentSpendB(5) <- spends on the Network - \ / - 4 5 - \ / - NewSpend(9) <- spend on the Network - | - 9 - | - AnotherUniquePubkey(9) <- refering to this new unspent key -``` - -After this final step, the transaction can be considered settled, and we have reached finality. - -``` - -recipient sender Network - | | | - | ----- share MainPubkey ----> | | - | | | - | | --- send Spends ----> | - | | | - | <---- send Transfer -------- | | - | | - | | - | ------------ verify Transfer ----------------------> | - | | <- at this point - | | the tx is settled - | ------------ send Spend to reissue to self --------> | - | ------------ verify spends ------------------------> | - | | - ===================== finality ===================== <- at this point - the funds are safe - -``` - -## Wallet - -Any wallet software managing ANT must hold and secure: -- the `MainSecretKey`: password encrypted on disk or hardware wallet (leaking it could result in loss of funds) -- the `DerivationIndex`es of `UniquePubkey`s it currently owns (leaking those could result in reduced anonymity) -- the ancestry data (parent spends) for each `UniquePubkey`s in order to build the `Spend`s for each of them - -After spending a `UniquePubkey`, the wallet should never spend it again as it will result in burning the money. - -After receiving a `Transfer`, it should: -- verify that the ancestor spends exist on the Network and are valid -- reissue the received amount to a new `UniquePubkey` by spending the received money immediately. This is necessary to prevent the original sender from burning the ancestors spends which would result in the recipient not being able to spend the money -- verify that it didn't do the reissue above already to avoid burning its own money - -All `DerivationIndex`es should be discarded without a trace (no cache/log) as soon as they are not useful anymore as this could result in a loss of privacy. - diff --git a/sn_transfers/benches/reissue.rs b/sn_transfers/benches/reissue.rs deleted file mode 100644 index 68cf4c4d87..0000000000 --- a/sn_transfers/benches/reissue.rs +++ /dev/null @@ -1,161 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. - -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -#![allow(clippy::from_iter_instead_of_collect, clippy::unwrap_used)] - -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use sn_transfers::{ - create_first_cash_note_from_key, rng, CashNote, DerivationIndex, MainSecretKey, NanoTokens, - SignedTransaction, SpendReason, -}; -use std::collections::BTreeSet; - -const N_OUTPUTS: u64 = 100; - -fn bench_reissue_1_to_100(c: &mut Criterion) { - // prepare transfer of genesis cashnote - let mut rng = rng::from_seed([0u8; 32]); - let (starting_cashnote, starting_main_key) = generate_cashnote(); - let main_pubkey = starting_main_key.main_pubkey(); - let recipients = (0..N_OUTPUTS) - .map(|_| { - ( - NanoTokens::from(1), - main_pubkey, - DerivationIndex::random(&mut rng), - false, - ) - }) - .collect::>(); - - // transfer to N_OUTPUTS recipients - let signed_tx = SignedTransaction::new( - vec![starting_cashnote], - recipients, - starting_main_key.main_pubkey(), - SpendReason::default(), - &starting_main_key, - ) - .expect("Transaction creation to succeed"); - - // simulate spentbook to check for double spends - let mut spentbook_node = BTreeSet::new(); - for spend in &signed_tx.spends { - if !spentbook_node.insert(*spend.unique_pubkey()) { - panic!("cashnote double spend"); - }; - } - - // bench verification - c.bench_function(&format!("reissue split 1 to {N_OUTPUTS}"), |b| { - #[cfg(unix)] - let guard = pprof::ProfilerGuard::new(100).unwrap(); - - b.iter(|| { - black_box(&signed_tx).verify().unwrap(); - }); - - #[cfg(unix)] - if let Ok(report) = guard.report().build() { - let file = - std::fs::File::create(format!("reissue_split_1_to_{N_OUTPUTS}.svg")).unwrap(); - report.flamegraph(file).unwrap(); - }; - }); -} - -fn bench_reissue_100_to_1(c: &mut Criterion) { - // prepare transfer of genesis cashnote to recipient_of_100_mainkey - let mut rng = rng::from_seed([0u8; 32]); - let (starting_cashnote, starting_main_key) = generate_cashnote(); - let recipient_of_100_mainkey = MainSecretKey::random_from_rng(&mut rng); - let recipients = (0..N_OUTPUTS) - .map(|_| { - ( - NanoTokens::from(1), - recipient_of_100_mainkey.main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ) - }) - .collect::>(); - - // transfer to N_OUTPUTS recipients derived from recipient_of_100_mainkey - let signed_tx = SignedTransaction::new( - vec![starting_cashnote], - recipients, - starting_main_key.main_pubkey(), - SpendReason::default(), - &starting_main_key, - ) - .expect("Transaction creation to succeed"); - - // simulate spentbook to check for double spends - let mut spentbook_node = BTreeSet::new(); - let signed_spends: BTreeSet<_> = signed_tx.spends.clone().into_iter().collect(); - for spend in signed_spends.into_iter() { - if !spentbook_node.insert(*spend.unique_pubkey()) { - panic!("cashnote double spend"); - }; - } - - // prepare to send all of those cashnotes back to our starting_main_key - let total_amount = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .sum(); - let many_cashnotes = signed_tx.output_cashnotes.into_iter().collect(); - let one_single_recipient = vec![( - NanoTokens::from(total_amount), - starting_main_key.main_pubkey(), - DerivationIndex::random(&mut rng), - false, - )]; - - // create transfer to merge all of the cashnotes into one - let many_to_one_tx = SignedTransaction::new( - many_cashnotes, - one_single_recipient, - starting_main_key.main_pubkey(), - SpendReason::default(), - &recipient_of_100_mainkey, - ) - .expect("Many to one Transaction creation to succeed"); - - // bench verification - c.bench_function(&format!("reissue merge {N_OUTPUTS} to 1"), |b| { - #[cfg(unix)] - let guard = pprof::ProfilerGuard::new(100).unwrap(); - - b.iter(|| { - black_box(&many_to_one_tx).verify().unwrap(); - }); - - #[cfg(unix)] - if let Ok(report) = guard.report().build() { - let file = - std::fs::File::create(format!("reissue_merge_{N_OUTPUTS}_to_1.svg")).unwrap(); - report.flamegraph(file).unwrap(); - }; - }); -} - -fn generate_cashnote() -> (CashNote, MainSecretKey) { - let key = MainSecretKey::random(); - let genesis = create_first_cash_note_from_key(&key).expect("Genesis creation to succeed."); - (genesis, key) -} - -criterion_group! { - name = reissue; - config = Criterion::default().sample_size(10); - targets = bench_reissue_1_to_100, bench_reissue_100_to_1 -} - -criterion_main!(reissue); diff --git a/sn_transfers/dag.svg b/sn_transfers/dag.svg deleted file mode 100644 index 8bf6eb99df..0000000000 --- a/sn_transfers/dag.svg +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - -c1f1425c1823e48475b0828fca5d324e0c7941dcb52379174bcbedf5f9be3be5 - - -SpendAddress(c1f142) - - - - -e8f83f264e29fe515cb343c4dd54d8d4d9db750a6e57437867e33dd30869bead - - -SpendAddress(e8f83f) - - - - -0->1 - - -NanoTokens(900000000000000000) - - - -883e2d37b1fdf3f4cc3b889c8c8b904e369a699e32f64294bd3cc771825960af - - -SpendAddress(883e2d) - - - - -0->2 - - -NanoTokens(388490188500000000) - - - -66268051e972c408c5f27777d6ce080d609891194af303a19558da1c76fe271a - - -SpendAddress(662680) - - - - -1->4 - - -NanoTokens(899999999000000000) - - - -ae3b39145533d45758543c7409f3de7a972b1dddfe3ea18c7825df9bccf73739 - - -SpendAddress(ae3b39) - - - - -1->7 - - -NanoTokens(1000000000) - - - -964d04e290a8fd960b08d90aba03a5ea01ad88f7af5f917f0433b5e9271f30c1 - - -SpendAddress(964d04) - - - - -2->3 - - -NanoTokens(388490188500000000) - - - -6391d9cfbc43964587e1ebb049430e9038f3635d22aa407a046c88de55ddd9f3 - - -SpendAddress(6391d9) - - - - -4->5 - - -NanoTokens(1000000000) - - - -0b9e3253b87e1f75d65d53d9579980339b6016a2db3e0b24d82fd8728377d285 - - -SpendAddress(0b9e32) - - - - -4->6 - - -NanoTokens(899999998000000000) - - - diff --git a/sn_transfers/src/cashnotes.rs b/sn_transfers/src/cashnotes.rs deleted file mode 100644 index 160099fb1b..0000000000 --- a/sn_transfers/src/cashnotes.rs +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -mod address; -mod cashnote; -mod hash; -mod nano; -mod signed_spend; -mod spend_reason; -mod unique_keys; - -pub use address::SpendAddress; -pub use cashnote::CashNote; -pub use hash::Hash; -pub use nano::NanoTokens; -pub use signed_spend::{SignedSpend, Spend}; -pub use spend_reason::SpendReason; -pub use unique_keys::{DerivationIndex, DerivedSecretKey, MainPubkey, MainSecretKey, UniquePubkey}; - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::TransferError; - - use std::collections::{BTreeMap, BTreeSet}; - - fn generate_parent_spends( - derived_sk: DerivedSecretKey, - amount: u64, - output: UniquePubkey, - ) -> BTreeSet { - let mut descendants = BTreeMap::new(); - let _ = descendants.insert(output, NanoTokens::from(amount)); - let spend = Spend { - unique_pubkey: derived_sk.unique_pubkey(), - reason: SpendReason::default(), - ancestors: BTreeSet::new(), - descendants, - royalties: vec![], - }; - let mut parent_spends = BTreeSet::new(); - let derived_key_sig = derived_sk.sign(&spend.to_bytes_for_signing()); - let _ = parent_spends.insert(SignedSpend { - spend, - derived_key_sig, - }); - parent_spends - } - - #[test] - fn from_hex_should_deserialize_a_hex_encoded_string_to_a_cashnote() -> Result<(), TransferError> - { - let mut rng = crate::rng::from_seed([0u8; 32]); - let amount = 1_530_000_000; - let main_key = MainSecretKey::random_from_rng(&mut rng); - let derivation_index = DerivationIndex::random(&mut rng); - let derived_key = main_key.derive_key(&derivation_index); - - let parent_spends = generate_parent_spends( - main_key.derive_key(&DerivationIndex::random(&mut rng)), - amount, - derived_key.unique_pubkey(), - ); - - let cashnote = CashNote { - parent_spends, - main_pubkey: main_key.main_pubkey(), - derivation_index, - }; - - let hex = cashnote.to_hex()?; - - let cashnote = CashNote::from_hex(&hex)?; - assert_eq!(cashnote.value().as_nano(), 1_530_000_000); - - Ok(()) - } - - #[test] - fn to_hex_should_serialize_a_cashnote_to_a_hex_encoded_string() -> Result<(), TransferError> { - let mut rng = crate::rng::from_seed([0u8; 32]); - let amount = 100; - let main_key = MainSecretKey::random_from_rng(&mut rng); - let derivation_index = DerivationIndex::random(&mut rng); - let derived_key = main_key.derive_key(&derivation_index); - - let parent_spends = generate_parent_spends( - main_key.derive_key(&DerivationIndex::random(&mut rng)), - amount, - derived_key.unique_pubkey(), - ); - - let cashnote = CashNote { - parent_spends, - main_pubkey: main_key.main_pubkey(), - derivation_index, - }; - - let hex = cashnote.to_hex()?; - let cashnote_from_hex = CashNote::from_hex(&hex)?; - - assert_eq!(cashnote.value(), cashnote_from_hex.value()); - - Ok(()) - } - - #[test] - fn input_should_error_if_unique_pubkey_is_not_derived_from_main_key( - ) -> Result<(), TransferError> { - let mut rng = crate::rng::from_seed([0u8; 32]); - let amount = 100; - - let main_key = MainSecretKey::random_from_rng(&mut rng); - let derivation_index = DerivationIndex::random(&mut rng); - let derived_key = main_key.derive_key(&derivation_index); - - let parent_spends = generate_parent_spends( - main_key.derive_key(&DerivationIndex::random(&mut rng)), - amount, - derived_key.unique_pubkey(), - ); - - let cashnote = CashNote { - parent_spends, - main_pubkey: main_key.main_pubkey(), - derivation_index, - }; - - let other_main_key = MainSecretKey::random_from_rng(&mut rng); - let result = cashnote.derived_key(&other_main_key); - assert!(matches!( - result, - Err(TransferError::MainSecretKeyDoesNotMatchMainPubkey) - )); - Ok(()) - } - - #[test] - fn test_cashnote_without_inputs_fails_verification() -> Result<(), TransferError> { - let mut rng = crate::rng::from_seed([0u8; 32]); - - let main_key = MainSecretKey::random_from_rng(&mut rng); - let derivation_index = DerivationIndex::random(&mut rng); - - let cashnote = CashNote { - parent_spends: Default::default(), - main_pubkey: main_key.main_pubkey(), - derivation_index, - }; - - assert!(matches!( - cashnote.verify(), - Err(TransferError::CashNoteMissingAncestors) - )); - - Ok(()) - } -} diff --git a/sn_transfers/src/cashnotes/address.rs b/sn_transfers/src/cashnotes/address.rs deleted file mode 100644 index a1f8812767..0000000000 --- a/sn_transfers/src/cashnotes/address.rs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{Result, TransferError}; - -use super::UniquePubkey; - -use serde::{Deserialize, Serialize}; -use std::{fmt, hash::Hash}; -use xor_name::XorName; - -/// The address of a SignedSpend in the network. -/// This is used to check if a CashNote is spent, note that the actual CashNote is not stored on the Network. -#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)] -pub struct SpendAddress(XorName); - -impl SpendAddress { - /// Construct a `SpendAddress` given an `XorName`. - pub fn new(name: XorName) -> Self { - Self(name) - } - - /// Construct a `SpendAddress` from a `UniquePubkey`. - pub fn from_unique_pubkey(unique_pubkey: &UniquePubkey) -> Self { - Self::new(XorName::from_content(&unique_pubkey.to_bytes())) - } - - /// Return the name, which is the hash of `UniquePubkey`. - pub fn xorname(&self) -> &XorName { - &self.0 - } - - pub fn to_hex(&self) -> String { - hex::encode(self.0) - } - - pub fn from_hex(hex: &str) -> Result { - let bytes = - hex::decode(hex).map_err(|e| TransferError::HexDeserializationFailed(e.to_string()))?; - let xorname = XorName(bytes.try_into().map_err(|_| { - TransferError::HexDeserializationFailed("wrong string size".to_string()) - })?); - Ok(Self::new(xorname)) - } -} - -impl std::fmt::Debug for SpendAddress { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SpendAddress({})", &self.to_hex()[0..6]) - } -} - -impl std::str::FromStr for SpendAddress { - type Err = TransferError; - - fn from_str(s: &str) -> Result { - let pk_res = UniquePubkey::from_hex(s); - let addr_res = SpendAddress::from_hex(s); - - match (pk_res, addr_res) { - (Ok(pk), _) => Ok(SpendAddress::from_unique_pubkey(&pk)), - (_, Ok(addr)) => Ok(addr), - _ => Err(TransferError::HexDeserializationFailed( - "Invalid SpendAddress".to_string(), - )), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bls::SecretKey; - use std::str::FromStr; - - #[test] - fn test_spend_address_hex_conversions() -> eyre::Result<()> { - let mut rng = rand::thread_rng(); - let spend_address = SpendAddress::new(XorName::random(&mut rng)); - let hex = spend_address.to_hex(); - let spend_address2 = SpendAddress::from_hex(&hex)?; - assert_eq!(spend_address, spend_address2); - Ok(()) - } - - #[test] - fn test_from_str() -> eyre::Result<()> { - let public_key = SecretKey::random().public_key(); - let unique_pk = UniquePubkey::new(public_key); - let spend_address = SpendAddress::from_unique_pubkey(&unique_pk); - let addr_hex = spend_address.to_hex(); - let unique_pk_hex = unique_pk.to_hex(); - - let addr = SpendAddress::from_str(&addr_hex)?; - assert_eq!(addr, spend_address); - - let addr2 = SpendAddress::from_str(&unique_pk_hex)?; - assert_eq!(addr2, spend_address); - Ok(()) - } -} diff --git a/sn_transfers/src/cashnotes/cashnote.rs b/sn_transfers/src/cashnotes/cashnote.rs deleted file mode 100644 index 9f464e0a44..0000000000 --- a/sn_transfers/src/cashnotes/cashnote.rs +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::{ - DerivationIndex, DerivedSecretKey, Hash, MainPubkey, MainSecretKey, NanoTokens, SignedSpend, - UniquePubkey, -}; - -use crate::{Result, TransferError}; - -use serde::{Deserialize, Serialize}; -use std::collections::BTreeSet; -use std::fmt::Debug; -use tiny_keccak::{Hasher, Sha3}; - -/// Represents a CashNote (CashNote). -/// -/// A CashNote is like a piece of money on an account. Only the owner can spend it. -/// -/// A CashNote has a MainPubkey representing the owner of the CashNote. -/// -/// An MainPubkey is a PublicKey. -/// The user who receives payments (`Transfer`) to this MainPubkey, will be holding -/// a MainSecretKey - a secret key, which corresponds to the MainPubkey. -/// -/// The MainPubkey can be given out to multiple parties and -/// multiple CashNotes can share the same MainPubkey. -/// -/// The Network nodes never sees the MainPubkey. Instead, when a -/// transaction output cashnote is created for a given MainPubkey, a random -/// derivation index is generated and used to derive a UniquePubkey, which will be -/// used to create the `Spend` for this new cashnote. -/// -/// The UniquePubkey is a unique identifier of a CashNote and its associated Spend (once the CashNote is spent). -/// So there can only ever be one CashNote with that id, previously, now and forever. -/// The UniquePubkey consists of a PublicKey. To unlock the tokens of the CashNote, -/// the corresponding DerivedSecretKey (consists of a SecretKey) must be used. -/// It is derived from the MainSecretKey, in the same way as the UniquePubkey was derived -/// from the MainPubkey to get the UniquePubkey. -/// -/// So, there are two important pairs to conceptually be aware of. -/// The MainSecretKey and MainPubkey is a unique pair of a user, where the MainSecretKey -/// is held secret, and the MainPubkey is given to all and anyone who wishes to send tokens to you. -/// A sender of tokens will derive the UniquePubkey from the MainPubkey, which will identify the CashNote that -/// holds the tokens going to the owner. The sender does this using a derivation index. -/// The owner of the tokens, will use the same derivation index, to derive the DerivedSecretKey -/// from the MainSecretKey. The DerivedSecretKey and UniquePubkey pair is the second important pair. -/// For an outsider, there is no way to associate either the DerivedSecretKey or the UniquePubkey to the MainPubkey -/// (or for that matter to the MainSecretKey, if they were ever to see it, which they shouldn't of course). -/// Only by having the derivation index, which is only known to sender and owner, can such a connection be made. -/// -/// To spend or work with a CashNote, wallet software must obtain the corresponding -/// MainSecretKey from the user, and then call an API function that accepts a MainSecretKey, -/// eg: `cashnote.derivation_index(&main_key)` -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Hash)] -pub struct CashNote { - /// The parent spends of this CashNote. These are assumed to fetched from the Network. - pub parent_spends: BTreeSet, - /// This is the MainPubkey of the owner of this CashNote - pub main_pubkey: MainPubkey, - /// The derivation index used to derive the UniquePubkey and DerivedSecretKey from the MainPubkey and MainSecretKey respectively. - /// It is to be kept secret to preserve the privacy of the owner. - /// Without it, it is very hard to link the MainPubkey (original owner) and the UniquePubkey (derived unique identity of the CashNote) - pub derivation_index: DerivationIndex, -} - -impl Debug for CashNote { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // print all fields and add unique_pubkey as first field - f.debug_struct("CashNote") - .field("unique_pubkey", &self.unique_pubkey()) - .field("main_pubkey", &self.main_pubkey) - .field("derivation_index", &self.derivation_index) - .field("parent_spends", &self.parent_spends) - .finish() - } -} - -impl CashNote { - /// Return the unique pubkey of this CashNote. - pub fn unique_pubkey(&self) -> UniquePubkey { - self.main_pubkey() - .new_unique_pubkey(&self.derivation_index()) - } - - // Return MainPubkey from which UniquePubkey is derived. - pub fn main_pubkey(&self) -> &MainPubkey { - &self.main_pubkey - } - - /// Return DerivedSecretKey using MainSecretKey supplied by caller. - /// Will return an error if the supplied MainSecretKey does not match the - /// CashNote MainPubkey. - pub fn derived_key(&self, main_key: &MainSecretKey) -> Result { - if &main_key.main_pubkey() != self.main_pubkey() { - return Err(TransferError::MainSecretKeyDoesNotMatchMainPubkey); - } - Ok(main_key.derive_key(&self.derivation_index())) - } - - /// Return UniquePubkey using MainPubkey supplied by caller. - /// Will return an error if the supplied MainPubkey does not match the - /// CashNote MainPubkey. - pub fn derived_pubkey(&self, main_pubkey: &MainPubkey) -> Result { - if main_pubkey != self.main_pubkey() { - return Err(TransferError::MainPubkeyMismatch); - } - Ok(main_pubkey.new_unique_pubkey(&self.derivation_index())) - } - - /// Return the derivation index that was used to derive UniquePubkey and corresponding DerivedSecretKey of a CashNote. - pub fn derivation_index(&self) -> DerivationIndex { - self.derivation_index - } - - /// Return the value in NanoTokens for this CashNote. - pub fn value(&self) -> NanoTokens { - let mut total_amount: u64 = 0; - for p in self.parent_spends.iter() { - let amount = p - .spend - .get_output_amount(&self.unique_pubkey()) - .unwrap_or(NanoTokens::zero()); - total_amount += amount.as_nano(); - } - NanoTokens::from(total_amount) - } - - /// Generate the hash of this CashNote - pub fn hash(&self) -> Hash { - let mut sha3 = Sha3::v256(); - sha3.update(&self.main_pubkey.to_bytes()); - sha3.update(&self.derivation_index.0); - - for sp in self.parent_spends.iter() { - sha3.update(&sp.to_bytes()); - } - - let mut hash = [0u8; 32]; - sha3.finalize(&mut hash); - Hash::from(hash) - } - - /// Verifies that this CashNote is valid. This checks that the CashNote is structurally sound. - /// Important: this does not check if the CashNote has been spent, nor does it check if the parent spends are spent. - /// For that, one must query the Network. - pub fn verify(&self) -> Result<(), TransferError> { - // check if we have parents - if self.parent_spends.is_empty() { - return Err(TransferError::CashNoteMissingAncestors); - } - - // check if the parents refer to us as a descendant - let unique_pubkey = self.unique_pubkey(); - if !self - .parent_spends - .iter() - .all(|p| p.spend.get_output_amount(&unique_pubkey).is_some()) - { - return Err(TransferError::InvalidParentSpend(format!( - "Parent spends refered in CashNote: {unique_pubkey:?} do not refer to its pubkey as an output" - ))); - } - - Ok(()) - } - - /// Deserializes a `CashNote` represented as a hex string to a `CashNote`. - pub fn from_hex(hex: &str) -> Result { - let mut bytes = - hex::decode(hex).map_err(|e| TransferError::HexDeserializationFailed(e.to_string()))?; - bytes.reverse(); - let cashnote: CashNote = rmp_serde::from_slice(&bytes) - .map_err(|e| TransferError::HexDeserializationFailed(e.to_string()))?; - Ok(cashnote) - } - - /// Serialize this `CashNote` instance to a hex string. - pub fn to_hex(&self) -> Result { - let mut serialized = rmp_serde::to_vec(&self) - .map_err(|e| TransferError::HexSerializationFailed(e.to_string()))?; - serialized.reverse(); - Ok(hex::encode(serialized)) - } -} diff --git a/sn_transfers/src/cashnotes/hash.rs b/sn_transfers/src/cashnotes/hash.rs deleted file mode 100644 index b0d795d0ec..0000000000 --- a/sn_transfers/src/cashnotes/hash.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use serde::{Deserialize, Serialize}; -use std::{fmt, str::FromStr}; - -use crate::TransferError; - -/// sha3 256 hash used for Spend Reasons, Transaction hashes, anything hash related in this crate -#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, Serialize, Deserialize)] -pub struct Hash([u8; 32]); - -impl Hash { - #[expect(clippy::self_named_constructors)] - /// sha3 256 hash - pub fn hash(input: &[u8]) -> Self { - Self::from(sha3_256(input)) - } - - /// Access the 32 byte slice of the hash - pub fn slice(&self) -> &[u8; 32] { - &self.0 - } - - /// Deserializes a `Hash` represented as a hex string to a `Hash`. - pub fn from_hex(hex: &str) -> Result { - let mut h = Self::default(); - hex::decode_to_slice(hex, &mut h.0) - .map_err(|e| TransferError::HexDeserializationFailed(e.to_string()))?; - Ok(h) - } - - /// Serialize this `Hash` instance to a hex string. - pub fn to_hex(&self) -> String { - hex::encode(self.0) - } -} - -impl FromStr for Hash { - type Err = TransferError; - - fn from_str(s: &str) -> std::result::Result { - Hash::from_hex(s) - } -} - -impl From<[u8; 32]> for Hash { - fn from(val: [u8; 32]) -> Hash { - Hash(val) - } -} - -// Display Hash value as hex in Debug output. consolidates 36 lines to 3 for pretty output -impl fmt::Debug for Hash { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("Hash").field(&self.to_hex()).finish() - } -} - -impl AsRef<[u8]> for Hash { - fn as_ref(&self) -> &[u8] { - &self.0 - } -} - -pub(crate) fn sha3_256(input: &[u8]) -> [u8; 32] { - use tiny_keccak::{Hasher, Sha3}; - - let mut sha3 = Sha3::v256(); - let mut output = [0; 32]; - sha3.update(input); - sha3.finalize(&mut output); - output -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn hash() { - let data = b"hello world"; - let expected = b"\ - \x64\x4b\xcc\x7e\x56\x43\x73\x04\x09\x99\xaa\xc8\x9e\x76\x22\xf3\ - \xca\x71\xfb\xa1\xd9\x72\xfd\x94\xa3\x1c\x3b\xfb\xf2\x4e\x39\x38\ - "; - assert_eq!(sha3_256(data), *expected); - - let hash = Hash::hash(data); - assert_eq!(hash.slice(), expected); - } - - #[test] - fn hex_encoding() { - let data = b"hello world"; - let expected_hex = "644bcc7e564373040999aac89e7622f3ca71fba1d972fd94a31c3bfbf24e3938"; - - let hash = Hash::hash(data); - - assert_eq!(hash.to_hex(), expected_hex.to_string()); - assert_eq!(Hash::from_hex(expected_hex), Ok(hash)); - - let too_long_hex = format!("{expected_hex}ab"); - assert_eq!( - Hash::from_hex(&too_long_hex), - Err(TransferError::HexDeserializationFailed( - "Invalid string length".to_string() - )) - ); - - assert_eq!( - Hash::from_hex(&expected_hex[0..30]), - Err(TransferError::HexDeserializationFailed( - "Invalid string length".to_string() - )) - ); - } -} diff --git a/sn_transfers/src/cashnotes/nano.rs b/sn_transfers/src/cashnotes/nano.rs deleted file mode 100644 index 2c9ff3e4b7..0000000000 --- a/sn_transfers/src/cashnotes/nano.rs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{Result, TransferError}; - -use serde::{Deserialize, Serialize}; -use std::{ - fmt::{self, Display, Formatter}, - str::FromStr, -}; - -/// The conversion from NanoTokens to raw value -const TOKEN_TO_RAW_POWER_OF_10_CONVERSION: u32 = 9; - -/// The conversion from NanoTokens to raw value -const TOKEN_TO_RAW_CONVERSION: u64 = 1_000_000_000; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] -/// An amount in SNT Nanos. 10^9 Nanos = 1 SNT. -pub struct NanoTokens(u64); - -impl NanoTokens { - /// Type safe representation of zero NanoTokens. - pub const fn zero() -> Self { - Self(0) - } - - /// Returns whether it's a representation of zero NanoTokens. - pub const fn is_zero(&self) -> bool { - self.0 == 0 - } - - /// New value from a number of nano tokens. - pub const fn from(value: u64) -> Self { - Self(value) - } - - /// Total NanoTokens expressed in number of nano tokens. - pub const fn as_nano(self) -> u64 { - self.0 - } - - /// Computes `self + rhs`, returning `None` if overflow occurred. - pub fn checked_add(self, rhs: NanoTokens) -> Option { - self.0.checked_add(rhs.0).map(Self::from) - } - - /// Computes `self - rhs`, returning `None` if overflow occurred. - pub fn checked_sub(self, rhs: NanoTokens) -> Option { - self.0.checked_sub(rhs.0).map(Self::from) - } - - /// Converts the Nanos into bytes - pub fn to_bytes(&self) -> [u8; 8] { - self.0.to_ne_bytes() - } -} - -impl From for NanoTokens { - fn from(value: u64) -> Self { - Self(value) - } -} - -impl FromStr for NanoTokens { - type Err = TransferError; - - fn from_str(value_str: &str) -> Result { - let mut itr = value_str.splitn(2, '.'); - let converted_units = { - let units = itr - .next() - .and_then(|s| s.parse::().ok()) - .ok_or_else(|| { - TransferError::FailedToParseNanoToken("Can't parse token units".to_string()) - })?; - - units - .checked_mul(TOKEN_TO_RAW_CONVERSION) - .ok_or(TransferError::ExcessiveNanoValue)? - }; - - let remainder = { - let remainder_str = itr.next().unwrap_or_default().trim_end_matches('0'); - - if remainder_str.is_empty() { - 0 - } else { - let parsed_remainder = remainder_str.parse::().map_err(|_| { - TransferError::FailedToParseNanoToken("Can't parse token remainder".to_string()) - })?; - - let remainder_conversion = TOKEN_TO_RAW_POWER_OF_10_CONVERSION - .checked_sub(remainder_str.len() as u32) - .ok_or(TransferError::LossOfNanoPrecision)?; - parsed_remainder * 10_u64.pow(remainder_conversion) - } - }; - - Ok(Self::from(converted_units + remainder)) - } -} - -impl Display for NanoTokens { - fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { - let unit = self.0 / TOKEN_TO_RAW_CONVERSION; - let remainder = self.0 % TOKEN_TO_RAW_CONVERSION; - write!(formatter, "{unit}.{remainder:09}") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn from_str() -> Result<()> { - assert_eq!(NanoTokens(0), NanoTokens::from_str("0")?); - assert_eq!(NanoTokens(0), NanoTokens::from_str("0.")?); - assert_eq!(NanoTokens(0), NanoTokens::from_str("0.0")?); - assert_eq!(NanoTokens(1), NanoTokens::from_str("0.000000001")?); - assert_eq!(NanoTokens(1_000_000_000), NanoTokens::from_str("1")?); - assert_eq!(NanoTokens(1_000_000_000), NanoTokens::from_str("1.")?); - assert_eq!(NanoTokens(1_000_000_000), NanoTokens::from_str("1.0")?); - assert_eq!( - NanoTokens(1_000_000_001), - NanoTokens::from_str("1.000000001")? - ); - assert_eq!(NanoTokens(1_100_000_000), NanoTokens::from_str("1.1")?); - assert_eq!( - NanoTokens(1_100_000_001), - NanoTokens::from_str("1.100000001")? - ); - assert_eq!( - NanoTokens(4_294_967_295_000_000_000), - NanoTokens::from_str("4294967295")? - ); - assert_eq!( - NanoTokens(4_294_967_295_999_999_999), - NanoTokens::from_str("4294967295.999999999")?, - ); - assert_eq!( - NanoTokens(4_294_967_295_999_999_999), - NanoTokens::from_str("4294967295.9999999990000")?, - ); - - assert_eq!( - Err(TransferError::FailedToParseNanoToken( - "Can't parse token units".to_string() - )), - NanoTokens::from_str("a") - ); - assert_eq!( - Err(TransferError::FailedToParseNanoToken( - "Can't parse token remainder".to_string() - )), - NanoTokens::from_str("0.a") - ); - assert_eq!( - Err(TransferError::FailedToParseNanoToken( - "Can't parse token remainder".to_string() - )), - NanoTokens::from_str("0.0.0") - ); - assert_eq!( - Err(TransferError::LossOfNanoPrecision), - NanoTokens::from_str("0.0000000009") - ); - assert_eq!( - Err(TransferError::ExcessiveNanoValue), - NanoTokens::from_str("18446744074") - ); - Ok(()) - } - - #[test] - fn display() { - assert_eq!("0.000000000", format!("{}", NanoTokens(0))); - assert_eq!("0.000000001", format!("{}", NanoTokens(1))); - assert_eq!("0.000000010", format!("{}", NanoTokens(10))); - assert_eq!("1.000000000", format!("{}", NanoTokens(1_000_000_000))); - assert_eq!("1.000000001", format!("{}", NanoTokens(1_000_000_001))); - assert_eq!( - "4294967295.000000000", - format!("{}", NanoTokens(4_294_967_295_000_000_000)) - ); - } - - #[test] - fn checked_add_sub() { - assert_eq!( - Some(NanoTokens(3)), - NanoTokens(1).checked_add(NanoTokens(2)) - ); - assert_eq!(None, NanoTokens(u64::MAX).checked_add(NanoTokens(1))); - assert_eq!(None, NanoTokens(u64::MAX).checked_add(NanoTokens(u64::MAX))); - - assert_eq!( - Some(NanoTokens(0)), - NanoTokens(u64::MAX).checked_sub(NanoTokens(u64::MAX)) - ); - assert_eq!(None, NanoTokens(0).checked_sub(NanoTokens(u64::MAX))); - assert_eq!(None, NanoTokens(10).checked_sub(NanoTokens(11))); - } -} diff --git a/sn_transfers/src/cashnotes/signed_spend.rs b/sn_transfers/src/cashnotes/signed_spend.rs deleted file mode 100644 index 63dabfef93..0000000000 --- a/sn_transfers/src/cashnotes/signed_spend.rs +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::spend_reason::SpendReason; -use super::{Hash, NanoTokens, UniquePubkey}; -use crate::{ - DerivationIndex, DerivedSecretKey, Result, Signature, SpendAddress, TransferError, - NETWORK_ROYALTIES_PK, -}; - -use custom_debug::Debug; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Ordering, - collections::{BTreeMap, BTreeSet}, -}; - -/// `SignedSpend`s are the core of the Network's transaction system. -/// They are the data type on the Network used to commit to a transfer of value. Analogous to a transaction in Bitcoin. -/// They are signed piece of data proving the owner's commitment to transfer value. -/// `Spend`s refer to their ancestors and descendants, forming a directed acyclic graph that starts from Genesis. -#[derive(Debug, Clone, PartialOrd, Ord, Serialize, Deserialize)] -pub struct SignedSpend { - /// The Spend, together with the owner's signature over it, constitutes the SignedSpend. - pub spend: Spend, - /// The DerivedSecretKey's signature over the Spend, proving the owner's commitment to the Spend. - #[debug(skip)] - pub derived_key_sig: Signature, -} - -impl SignedSpend { - /// Create a new SignedSpend - pub fn sign(spend: Spend, sk: &DerivedSecretKey) -> Self { - let derived_key_sig = sk.sign(&spend.to_bytes_for_signing()); - Self { - spend, - derived_key_sig, - } - } - - /// Get public key of input CashNote. - pub fn unique_pubkey(&self) -> &UniquePubkey { - &self.spend.unique_pubkey - } - - /// Get the SpendAddress where this Spend shoud be - pub fn address(&self) -> SpendAddress { - SpendAddress::from_unique_pubkey(&self.spend.unique_pubkey) - } - - /// Get Nano - pub fn amount(&self) -> NanoTokens { - self.spend.amount() - } - - /// Get reason. - pub fn reason(&self) -> &SpendReason { - &self.spend.reason - } - - /// Represent this SignedSpend as bytes. - pub fn to_bytes(&self) -> Vec { - let mut bytes: Vec = Default::default(); - bytes.extend(self.spend.to_bytes_for_signing()); - bytes.extend(self.derived_key_sig.to_bytes()); - bytes - } - - /// Verify a SignedSpend - /// - /// Checks that: - /// - it was signed by the DerivedSecretKey that owns the CashNote for this Spend - /// - the signature is valid - /// - /// It does NOT check: - /// - if the spend exists on the Network - /// - the spend's parents and if they exist on the Network - pub fn verify(&self) -> Result<()> { - // check signature - // the spend is signed by the DerivedSecretKey - // corresponding to the UniquePubkey of the CashNote being spent. - if self - .spend - .unique_pubkey - .verify(&self.derived_key_sig, self.spend.to_bytes_for_signing()) - { - Ok(()) - } else { - Err(TransferError::InvalidSpendSignature(*self.unique_pubkey())) - } - } - - /// Verify the parents of this Spend, making sure the input parent_spends are ancestors of self. - /// - Also handles the case of parent double spends. - /// - verifies that the parent_spends contains self as an output - /// - verifies the sum of total inputs equals to the sum of outputs - pub fn verify_parent_spends(&self, parent_spends: &BTreeSet) -> Result<()> { - let unique_key = self.unique_pubkey(); - trace!("Verifying parent_spends for {self:?}"); - - // sort parents by key (identify double spent parents) - let mut parents_by_key = BTreeMap::new(); - for s in parent_spends { - parents_by_key - .entry(s.unique_pubkey()) - .or_insert_with(Vec::new) - .push(s); - } - - let mut total_inputs: u64 = 0; - for (_, spends) in parents_by_key { - // check for double spend parents - if spends.len() > 1 { - error!("While verifying parents of {unique_key}, found a double spend parent: {spends:?}"); - return Err(TransferError::DoubleSpentParent); - } - - // check that the parent refers to self - if let Some(parent) = spends.first() { - match parent.spend.get_output_amount(unique_key) { - Some(amount) => { - total_inputs += amount.as_nano(); - } - None => { - return Err(TransferError::InvalidParentSpend(format!( - "Parent spend {:?} doesn't contain self spend {unique_key:?} as one of its output", - parent.unique_pubkey() - ))); - } - } - } - } - - let total_outputs = self.amount().as_nano(); - if total_outputs != total_inputs { - return Err(TransferError::InvalidParentSpend(format!( - "Parents total input value {total_inputs:?} doesn't match Spend's value {total_outputs:?}" - ))); - } - - trace!("Validated parent_spends for {unique_key}"); - Ok(()) - } - - /// Create a random Spend for testing - #[cfg(test)] - pub(crate) fn random_spend_to( - rng: &mut rand::prelude::ThreadRng, - output: UniquePubkey, - value: u64, - ) -> Self { - use crate::MainSecretKey; - - let sk = MainSecretKey::random(); - let index = DerivationIndex::random(rng); - let derived_sk = sk.derive_key(&index); - let unique_pubkey = derived_sk.unique_pubkey(); - let reason = SpendReason::default(); - let ancestor = MainSecretKey::random() - .derive_key(&DerivationIndex::random(rng)) - .unique_pubkey(); - let spend = Spend { - unique_pubkey, - reason, - ancestors: BTreeSet::from_iter(vec![ancestor]), - descendants: BTreeMap::from_iter(vec![(output, (NanoTokens::from(value)))]), - royalties: vec![], - }; - let derived_key_sig = derived_sk.sign(&spend.to_bytes_for_signing()); - Self { - spend, - derived_key_sig, - } - } -} - -// Impl manually to avoid clippy complaint about Hash conflict. -impl PartialEq for SignedSpend { - fn eq(&self, other: &Self) -> bool { - self.spend == other.spend && self.derived_key_sig == other.derived_key_sig - } -} - -impl Eq for SignedSpend {} - -impl std::hash::Hash for SignedSpend { - fn hash(&self, state: &mut H) { - let bytes = self.to_bytes(); - bytes.hash(state); - } -} - -/// Represents a spent UniquePubkey on the Network. -/// When a CashNote is spent, a Spend is created with the UniquePubkey of the CashNote. -/// It is then sent to the Network along with the signature of the owner using the DerivedSecretKey matching its UniquePubkey. -/// A Spend can have multiple ancestors (other spends) which will refer to it as a descendant. -/// A Spend's value is equal to the total value given by its ancestors, which one can fetch on the Network to check. -/// A Spend can have multiple descendants (other spends) which will refer to it as an ancestor. -/// A Spend's value is equal to the total value of given to its descendants. -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct Spend { - /// UniquePubkey of input CashNote that this SignedSpend is proving to be spent. - pub unique_pubkey: UniquePubkey, - /// Reason why this CashNote was spent. - pub reason: SpendReason, - /// parent spends of this spend - pub ancestors: BTreeSet, - /// spends we are parents of along with the amount we commited to give them - pub descendants: BTreeMap, - /// royalties outputs' derivation indexes - pub royalties: Vec, -} - -impl core::fmt::Debug for Spend { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Spend({:?}({:?}))", self.unique_pubkey, self.hash()) - } -} - -impl Spend { - /// Represent this Spend as bytes. - /// There is no from_bytes, because this function is not symetric as it uses hashes - pub fn to_bytes_for_signing(&self) -> Vec { - let mut bytes: Vec = Default::default(); - bytes.extend(self.unique_pubkey.to_bytes()); - bytes.extend(self.reason.hash().as_ref()); - bytes.extend("ancestors".as_bytes()); - for ancestor in self.ancestors.iter() { - bytes.extend(&ancestor.to_bytes()); - } - bytes.extend("descendants".as_bytes()); - for (descendant, amount) in self.descendants.iter() { - bytes.extend(&descendant.to_bytes()); - bytes.extend(amount.to_bytes()); - } - bytes.extend("royalties".as_bytes()); - for royalty in self.royalties.iter() { - bytes.extend(royalty.as_bytes()); - } - bytes - } - - /// represent this Spend as a Hash - pub fn hash(&self) -> Hash { - Hash::hash(&self.to_bytes_for_signing()) - } - - /// Returns the amount to be spent in this Spend - pub fn amount(&self) -> NanoTokens { - let amount: u64 = self - .descendants - .values() - .map(|amount| amount.as_nano()) - .sum(); - NanoTokens::from(amount) - } - - /// Returns the royalties descendants of this Spend - pub fn network_royalties(&self) -> BTreeSet<(UniquePubkey, NanoTokens, DerivationIndex)> { - let roy_pks: BTreeMap = self - .royalties - .iter() - .map(|di| (NETWORK_ROYALTIES_PK.new_unique_pubkey(di), *di)) - .collect(); - self.descendants - .iter() - .filter_map(|(pk, amount)| roy_pks.get(pk).map(|di| (*pk, *amount, *di))) - .collect() - } - - /// Returns the amount of a particual output target. - /// None if the target is not one of the outputs - pub fn get_output_amount(&self, target: &UniquePubkey) -> Option { - self.descendants.get(target).copied() - } -} - -impl PartialOrd for Spend { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Spend { - fn cmp(&self, other: &Self) -> Ordering { - self.unique_pubkey.cmp(&other.unique_pubkey) - } -} diff --git a/sn_transfers/src/cashnotes/spend_reason.rs b/sn_transfers/src/cashnotes/spend_reason.rs deleted file mode 100644 index 1761ef1353..0000000000 --- a/sn_transfers/src/cashnotes/spend_reason.rs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use bls::{Ciphertext, PublicKey, SecretKey}; -use serde::{Deserialize, Serialize}; -use xor_name::XorName; - -use crate::{DerivationIndex, Hash, Result, TransferError}; - -const CUSTOM_SPEND_REASON_SIZE: usize = 64; - -/// The attached metadata or reason for which a Spend was spent -#[derive(Default, Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub enum SpendReason { - #[default] - None, - /// Reference to network data - NetworkData(XorName), - /// Custom field for any application data - Custom(#[serde(with = "serde_bytes")] [u8; CUSTOM_SPEND_REASON_SIZE]), - - /// Beta only feature to track rewards - /// Discord username encrypted to the Foundation's pubkey with a random nonce - BetaRewardTracking(DiscordNameCipher), -} - -impl SpendReason { - pub fn hash(&self) -> Hash { - match self { - Self::None => Hash::default(), - Self::NetworkData(xor_name) => Hash::hash(xor_name), - Self::Custom(bytes) => Hash::hash(bytes), - Self::BetaRewardTracking(cypher) => Hash::hash(&cypher.cipher), - } - } - - pub fn create_reward_tracking_reason(input_str: &str) -> Result { - let input_pk = crate::PAYMENT_FORWARD_PK.public_key(); - Ok(Self::BetaRewardTracking(DiscordNameCipher::create( - input_str, input_pk, - )?)) - } - - pub fn decrypt_discord_cypher(&self, sk: &SecretKey) -> Option { - match self { - Self::BetaRewardTracking(cypher) => { - if let Ok(hash) = cypher.decrypt_to_username_hash(sk) { - Some(hash) - } else { - error!("Failed to decrypt BetaRewardTracking"); - None - } - } - _ => None, - } - } -} - -const MAX_CIPHER_SIZE: usize = u8::MAX as usize; -const DERIVATION_INDEX_SIZE: usize = 32; -const HASH_SIZE: usize = 32; -const CHECK_SUM_SIZE: usize = 8; -const CONTENT_SIZE: usize = HASH_SIZE + DERIVATION_INDEX_SIZE; -const LIMIT_SIZE: usize = CONTENT_SIZE + CHECK_SUM_SIZE; -const CHECK_SUM: [u8; CHECK_SUM_SIZE] = [15; CHECK_SUM_SIZE]; - -/// Discord username encrypted to the Foundation's pubkey with a random nonce -#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] -pub struct DiscordNameCipher { - /// Length of the cipher, hard limited to MAX_U8 - len: u8, - /// Encrypted Discord username - #[serde(with = "serde_bytes")] - cipher: [u8; MAX_CIPHER_SIZE], -} - -/// Discord username hash and nonce -/// u256 hash + u256 nonce might be overkill (very big) -struct DiscordName { - hash: Hash, - nonce: DerivationIndex, - checksum: [u8; CHECK_SUM_SIZE], -} - -impl DiscordName { - fn new(user_name: &str) -> Self { - let rng = &mut rand::thread_rng(); - DiscordName { - hash: Hash::hash(user_name.as_bytes()), - nonce: DerivationIndex::random(rng), - checksum: CHECK_SUM, - } - } - - fn to_sized_bytes(&self) -> [u8; LIMIT_SIZE] { - let mut bytes: [u8; LIMIT_SIZE] = [0; LIMIT_SIZE]; - bytes[0..HASH_SIZE].copy_from_slice(self.hash.slice()); - bytes[HASH_SIZE..CONTENT_SIZE].copy_from_slice(&self.nonce.0); - bytes[CONTENT_SIZE..LIMIT_SIZE].copy_from_slice(&self.checksum); - bytes - } - - fn from_bytes(bytes: &[u8]) -> Result { - let mut hash_bytes = [0; HASH_SIZE]; - hash_bytes.copy_from_slice(&bytes[0..HASH_SIZE]); - let hash = Hash::from(hash_bytes.to_owned()); - let mut nonce_bytes = [0; DERIVATION_INDEX_SIZE]; - nonce_bytes.copy_from_slice(&bytes[HASH_SIZE..CONTENT_SIZE]); - let nonce = DerivationIndex(nonce_bytes.to_owned()); - - let mut checksum = [0; CHECK_SUM_SIZE]; - if bytes.len() < LIMIT_SIZE { - // Backward compatible, which will allow invalid key generate a random hash result - checksum = CHECK_SUM; - } else { - checksum.copy_from_slice(&bytes[CONTENT_SIZE..LIMIT_SIZE]); - if checksum != CHECK_SUM { - return Err(TransferError::InvalidDecryptionKey); - } - } - - Ok(Self { - hash, - nonce, - checksum, - }) - } -} - -impl DiscordNameCipher { - /// Create a new DiscordNameCipher from a Discord username - /// it is encrypted to the given pubkey - pub fn create(user_name: &str, encryption_pk: PublicKey) -> Result { - let discord_name = DiscordName::new(user_name); - let cipher = encryption_pk.encrypt(discord_name.to_sized_bytes()); - let bytes = cipher.to_bytes(); - if bytes.len() > MAX_CIPHER_SIZE { - return Err(TransferError::DiscordNameCipherTooBig); - } - let mut sized = [0; MAX_CIPHER_SIZE]; - sized[0..bytes.len()].copy_from_slice(&bytes); - Ok(Self { - len: bytes.len() as u8, - cipher: sized, - }) - } - - /// Recover a Discord username hash using the secret key it was encrypted to - pub fn decrypt_to_username_hash(&self, sk: &SecretKey) -> Result { - let cipher = Ciphertext::from_bytes(&self.cipher[0..self.len as usize])?; - let decrypted = sk - .decrypt(&cipher) - .ok_or(TransferError::UserNameDecryptFailed)?; - let discord_name = DiscordName::from_bytes(&decrypted)?; - Ok(discord_name.hash) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_discord_name_cyphering() { - let encryption_sk = SecretKey::random(); - let encryption_pk = encryption_sk.public_key(); - - let user_name = "JohnDoe#1234"; - let user_name_hash = Hash::hash(user_name.as_bytes()); - let cypher = - DiscordNameCipher::create(user_name, encryption_pk).expect("cypher creation failed"); - let recovered_hash = cypher - .decrypt_to_username_hash(&encryption_sk) - .expect("decryption failed"); - assert_eq!(user_name_hash, recovered_hash); - - let user_name2 = "JackMa#5678"; - let user_name_hash2 = Hash::hash(user_name2.as_bytes()); - let cypher = - DiscordNameCipher::create(user_name2, encryption_pk).expect("cypher creation failed"); - let recovered_hash = cypher - .decrypt_to_username_hash(&encryption_sk) - .expect("decryption failed"); - assert_eq!(user_name_hash2, recovered_hash); - - assert_ne!(user_name_hash, user_name_hash2); - - let encryption_wrong_pk = SecretKey::random().public_key(); - let cypher_wrong = DiscordNameCipher::create(user_name, encryption_wrong_pk) - .expect("cypher creation failed"); - assert_eq!( - Err(TransferError::InvalidDecryptionKey), - cypher_wrong.decrypt_to_username_hash(&encryption_sk) - ); - } -} diff --git a/sn_transfers/src/cashnotes/unique_keys.rs b/sn_transfers/src/cashnotes/unique_keys.rs deleted file mode 100644 index 8be6eecd22..0000000000 --- a/sn_transfers/src/cashnotes/unique_keys.rs +++ /dev/null @@ -1,397 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::rand::{distributions::Standard, Rng, RngCore}; -use crate::wallet::{Error, Result}; - -use bls::{serde_impl::SerdeSecret, PublicKey, SecretKey, PK_SIZE}; -use serde::{Deserialize, Serialize}; -use std::fmt; - -/// This is used to generate a new UniquePubkey -/// from a MainPubkey, and the corresponding -/// DerivedSecretKey from the MainSecretKey of that MainPubkey. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Hash)] -pub struct DerivationIndex(pub [u8; 32]); - -impl fmt::Debug for DerivationIndex { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!( - formatter, - "{:02x}{:02x}{:02x}..", - self.0[0], self.0[1], self.0[2] - ) - } -} - -impl DerivationIndex { - /// generates a random derivation index - pub fn random(rng: &mut impl RngCore) -> DerivationIndex { - let mut bytes = [0u8; 32]; - rng.fill_bytes(&mut bytes); - DerivationIndex(bytes) - } - - /// returns the inner bytes representation - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } -} - -/// A Unique Public Key is the unique identifier of a CashNote and its SignedSpend on the Network when it is spent. -/// It is the mechanism that makes transactions untraceable to the real owner (MainPubkey). -/// It is the equivalent to using a different key for each transaction in bitcoin. -#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct UniquePubkey(PublicKey); - -impl UniquePubkey { - pub fn new>(public_key: G) -> Self { - Self(public_key.into()) - } - - pub fn to_bytes(&self) -> [u8; bls::PK_SIZE] { - self.0.to_bytes() - } - - /// Returns `true` if the signature matches the message. - pub fn verify>(&self, sig: &bls::Signature, msg: M) -> bool { - self.0.verify(sig, msg) - } - - pub fn public_key(&self) -> PublicKey { - self.0 - } - - pub fn to_hex(&self) -> String { - hex::encode(self.0.to_bytes()) - } - - pub fn from_hex>(hex: T) -> Result { - let public_key = bls_public_from_hex(hex)?; - Ok(Self::new(public_key)) - } -} - -/// Custom implementation of Serialize and Deserialize for UniquePubkey to make it an actionable -/// hex string that can be copy pasted in apps, instead of a useless array of numbers -/// Caveat: this is slower than the default implementation -impl Serialize for UniquePubkey { - fn serialize(&self, serializer: S) -> Result { - serializer.serialize_str(&self.to_hex()) - } -} - -impl<'de> Deserialize<'de> for UniquePubkey { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - // Backwards compatible deserialize - // this was implemented to support the old serialisation format as well - #[derive(Deserialize)] - #[serde(remote = "UniquePubkey")] - struct UniquePubkeyRep(PublicKey); - impl<'de> Deserialize<'de> for UniquePubkeyRep { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let key = ::deserialize(deserializer)?; - Ok(UniquePubkeyRep(key)) - } - } - - let deserialized = serde_json::Value::deserialize(deserializer)?; - - // the new serialisation format is a string - if deserialized.is_string() { - let hex: String = serde::Deserialize::deserialize(deserialized).map_err(|e| { - serde::de::Error::custom(format!( - "Failed to deserialize UniquePubkey string representation: {e}", - )) - })?; - UniquePubkey::from_hex(hex).map_err(|e| { - serde::de::Error::custom(format!( - "Failed to deserialize UniquePubkey from hex: {e}", - )) - }) - // the old serialisation format is an array - } else if deserialized.is_array() { - warn!("Detected old serialisation format for UniquePubkey, please update to the new format!"); - let key: UniquePubkeyRep = - serde::Deserialize::deserialize(deserialized).map_err(|e| { - serde::de::Error::custom(format!( - "Failed to deserialize UniquePubkey array representation: {e}", - )) - })?; - Ok(UniquePubkey(key.0)) - } else { - Err(serde::de::Error::custom( - "Failed to deserialize UniquePubkey: unknown serialisation format", - )) - } - } -} - -/// Actionable way to print a UniquePubkey -/// This way to print it is lengthier but allows to copy/paste it into the safe cli or other apps -/// To use for verification purposes -impl std::fmt::Debug for UniquePubkey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -impl std::fmt::Display for UniquePubkey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -/// This is the key that unlocks the value of a CashNote. -/// Holding this key gives you access to the tokens of the -/// CashNote with the corresponding UniquePubkey. -/// Like with the keys to your house or a safe, this is not something you share publicly. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DerivedSecretKey(SerdeSecret); - -impl DerivedSecretKey { - pub fn new>(secret_key: S) -> Self { - Self(SerdeSecret(secret_key.into())) - } - - /// This is the unique identifier of the CashNote that - /// this instance of CashNote secret key unlocks. - /// The CashNote does not exist until someone has sent tokens to it. - pub fn unique_pubkey(&self) -> UniquePubkey { - UniquePubkey(self.0.public_key()) - } - - /// Return the inner secret key - pub fn secret_key(&self) -> SecretKey { - self.0.inner().to_owned() - } - - pub(crate) fn sign(&self, msg: &[u8]) -> bls::Signature { - self.0.sign(msg) - } -} - -/// This is the MainPubkey to which tokens are send. -/// -/// The MainPubkey may be published and multiple payments sent to this address by various parties. -/// It is useful for accepting donations, for example. -/// -/// The CashNote can only be spent by the party holding the MainSecretKey that corresponds to the -/// MainPubkey, ie the CashNote recipient. -/// -/// This MainPubkey is only a client/wallet concept. It is NOT actually used in the transaction -/// and never seen by the spentbook nodes. -/// -/// The UniquePubkey used in the transaction is derived from this MainPubkey using a random -/// derivation index, which is stored in derivation_index. -/// -/// When someone wants to send tokens to this MainPubkey, -/// they generate the id of the CashNote - the UniquePubkey - that shall hold the tokens. -/// The UniquePubkey is generated from this MainPubkey, and only the sender -/// will at this point know that the UniquePubkey is related to this MainPubkey. -/// When creating the CashNote using that UniquePubkey, the sender will also include the -/// DerivationIndex that was used to generate the UniquePubkey, so that the recipient behind -/// the MainPubkey can also see that the UniquePubkey is related to this MainPubkey. -/// The recipient can then use the received DerivationIndex to generate the DerivedSecretKey -/// corresponding to that UniquePubkey, and thus unlock the value of the CashNote by using that DerivedSecretKey. -#[derive(Copy, PartialEq, Eq, Ord, PartialOrd, Clone, Serialize, Deserialize, Hash)] -pub struct MainPubkey(pub PublicKey); - -impl MainPubkey { - pub fn new(public_key: PublicKey) -> Self { - Self(public_key) - } - - /// Verify that the signature is valid for the message. - pub fn verify(&self, sig: &bls::Signature, msg: &[u8]) -> bool { - self.0.verify(sig, msg) - } - - /// Generate a new UniquePubkey from provided DerivationIndex. - /// This is supposed to be a unique identifier of a CashNote. - /// A new CashNote id is generated by someone who wants to send tokens to the MainPubkey. - /// When they create the new CashNote they will use this id, but that only works if this id was never used before. - pub fn new_unique_pubkey(&self, index: &DerivationIndex) -> UniquePubkey { - UniquePubkey(self.0.derive_child(&index.0)) - } - - pub fn to_bytes(self) -> [u8; PK_SIZE] { - self.0.to_bytes() - } - - // Get the underlying PublicKey - pub fn public_key(&self) -> PublicKey { - self.0 - } - - pub fn to_hex(&self) -> String { - hex::encode(self.0.to_bytes()) - } - - pub fn from_hex>(hex: T) -> Result { - let public_key = bls_public_from_hex(hex)?; - Ok(Self::new(public_key)) - } -} - -impl std::fmt::Debug for MainPubkey { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.to_hex()) - } -} - -/// A CashNote MainSecretKey is held by anyone who wants to -/// send or receive tokens using CashNotes. It is held privately -/// and not shared with anyone. -/// -/// The secret MainSecretKey has a static MainPubkey, which -/// is shared with others in order to receive payments. -/// With this MainSecretKey, new DerivedSecretKey:UniquePubkey pairs can be generated. -pub struct MainSecretKey(SerdeSecret); - -impl MainSecretKey { - /// Create a new MainSecretKey from a bls SecretKey. - pub fn new(secret_key: SecretKey) -> Self { - Self(SerdeSecret(secret_key)) - } - - /// Get the secret key. - pub fn secret_key(&self) -> &SecretKey { - &self.0 - } - - /// This is the static public address which is shared with others, and - /// to which payments can be made by getting a new unique identifier for a CashNote to be created. - pub fn main_pubkey(&self) -> MainPubkey { - MainPubkey(self.0.public_key()) - } - - /// Sign a message with the main key. - pub fn sign(&self, msg: &[u8]) -> bls::Signature { - self.0.sign(msg) - } - - /// Derive the key - the DerivedSecretKey - corresponding to a UniquePubkey - /// which was also derived using the same DerivationIndex. - /// - /// When someone wants to send tokens to the MainPubkey of this MainSecretKey, - /// they generate the id of the CashNote - the UniquePubkey - that shall hold the tokens. - /// The recipient of the tokens, is the person/entity that holds this MainSecretKey. - /// - /// The created CashNote contains the derivation index that was used to - /// generate that very UniquePubkey. - /// - /// When passing the derivation index to this function (`fn derive_key`), - /// a DerivedSecretKey is generated corresponding to the UniquePubkey. This DerivedSecretKey can unlock the CashNote of that - /// UniquePubkey, thus giving access to the tokens it holds. - /// By that, the recipient has received the tokens from the sender. - pub fn derive_key(&self, index: &DerivationIndex) -> DerivedSecretKey { - DerivedSecretKey::new(self.0.inner().derive_child(&index.0)) - } - - /// Represent as bytes. - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - pub fn random() -> Self { - Self::new(bls::SecretKey::random()) - } - - /// Create a randomly generated MainSecretKey. - pub fn random_from_rng(rng: &mut impl RngCore) -> Self { - let sk: SecretKey = rng.sample(Standard); - Self::new(sk) - } - - pub fn random_derived_key(&self, rng: &mut impl RngCore) -> DerivedSecretKey { - self.derive_key(&DerivationIndex::random(rng)) - } -} - -/// Construct a BLS public key from a hex-encoded string. -fn bls_public_from_hex>(hex: T) -> Result { - let bytes = hex::decode(hex).map_err(|_| Error::FailedToDecodeHexToKey)?; - let bytes_fixed_len: [u8; bls::PK_SIZE] = bytes - .as_slice() - .try_into() - .map_err(|_| Error::FailedToParseBlsKey)?; - let pk = bls::PublicKey::from_bytes(bytes_fixed_len)?; - Ok(pk) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_pubkeys_hex_conversion() -> eyre::Result<()> { - let sk = bls::SecretKey::random(); - let pk = sk.public_key(); - let main_pubkey = MainPubkey::new(pk); - let unique_pubkey = - main_pubkey.new_unique_pubkey(&DerivationIndex::random(&mut rand::thread_rng())); - - let main_pubkey_hex = main_pubkey.to_hex(); - let unique_pubkey_hex = unique_pubkey.to_hex(); - - let main_pubkey_from_hex = MainPubkey::from_hex(main_pubkey_hex)?; - let unique_pubkey_from_hex = UniquePubkey::from_hex(unique_pubkey_hex)?; - - assert_eq!(main_pubkey, main_pubkey_from_hex); - assert_eq!(unique_pubkey, unique_pubkey_from_hex); - Ok(()) - } - - #[test] - fn test_backwards_compatibility_deserialisation() -> eyre::Result<()> { - let pk = bls::SecretKey::random().public_key(); - let main_pubkey = MainPubkey::new(pk); - let unique_pk = - main_pubkey.new_unique_pubkey(&DerivationIndex::random(&mut rand::thread_rng())); - - // make sure str deserialisation works - let str_serialised = serde_json::to_string(&unique_pk)?; - println!("str_serialised: {str_serialised}"); - let str_deserialised: UniquePubkey = serde_json::from_str(&str_serialised)?; - assert_eq!(str_deserialised, unique_pk); - - // make sure array deserialisation works - let array_serialised = serde_json::to_string(&unique_pk.0)?; - println!("array_serialised: {array_serialised}"); - let array_deserialised: UniquePubkey = serde_json::from_str(&array_serialised)?; - assert_eq!(array_deserialised, unique_pk); - - Ok(()) - } - - #[test] - fn verification_using_child_key() -> eyre::Result<()> { - let msg = "just a test string".as_bytes(); - let main_sk = MainSecretKey::random(); - let derived_sk = main_sk.random_derived_key(&mut rand::thread_rng()); - - // Signature signed by parent key can not be verified by the child key. - let signature = main_sk.sign(msg); - assert!(main_sk.main_pubkey().verify(&signature, msg)); - assert!(!derived_sk.unique_pubkey().verify(&signature, msg)); - - // Signature signed by child key can not be verified by the parent key. - let signature = derived_sk.sign(msg); - assert!(derived_sk.unique_pubkey().verify(&signature, msg)); - assert!(!main_sk.main_pubkey().verify(&signature, msg)); - - Ok(()) - } -} diff --git a/sn_transfers/src/error.rs b/sn_transfers/src/error.rs deleted file mode 100644 index 6c5edbad0d..0000000000 --- a/sn_transfers/src/error.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{NanoTokens, UniquePubkey}; -use thiserror::Error; - -/// Specialisation of `std::Result`. -pub type Result = std::result::Result; - -#[derive(Error, Debug, Clone, PartialEq)] -#[non_exhaustive] -/// Transfer errors -pub enum TransferError { - #[error("Lost precision on the number of coins during parsing.")] - LossOfNanoPrecision, - #[error("The token amount would exceed the maximum value (u64::MAX).")] - ExcessiveNanoValue, - #[error("Failed to parse: {0}")] - FailedToParseNanoToken(String), - #[error("Invalid Spend: value was tampered with {0:?}")] - InvalidSpendValue(UniquePubkey), - #[error("Invalid parent spend: {0}")] - InvalidParentSpend(String), - #[error("Parent spend was double spent")] - DoubleSpentParent, - #[error("Invalid Spend Signature for {0:?}")] - InvalidSpendSignature(UniquePubkey), - #[error("Main key does not match public address.")] - MainSecretKeyDoesNotMatchMainPubkey, - #[error("Main pub key does not match.")] - MainPubkeyMismatch, - #[error("Could not deserialize specified hex string to a CashNote: {0}")] - HexDeserializationFailed(String), - #[error("Could not serialize CashNote to hex: {0}")] - HexSerializationFailed(String), - #[error("CashNote must have at least one ancestor.")] - CashNoteMissingAncestors, - #[error("The spends don't match the inputs of the Transaction.")] - SpendsDoNotMatchInputs, - #[error("Overflow occurred while adding values")] - NumericOverflow, - #[error("Not enough balance, {0} available, {1} required")] - NotEnoughBalance(NanoTokens, NanoTokens), - - #[error("CashNoteRedemption serialisation failed")] - CashNoteRedemptionSerialisationFailed, - #[error("CashNoteRedemption decryption failed")] - CashNoteRedemptionDecryptionFailed, - #[error("CashNoteRedemption encryption failed")] - CashNoteRedemptionEncryptionFailed, - - #[error("Transaction serialization error: {0}")] - TransactionSerialization(String), - #[error("Unsigned transaction is invalid: {0}")] - InvalidUnsignedTransaction(String), - #[error("Cannot create a Transaction with outputs equal to zero")] - ZeroOutputs, - - #[error("Transfer serialisation failed")] - TransferSerializationFailed, - #[error("Transfer deserialisation failed")] - TransferDeserializationFailed, - - #[error("Bls error: {0}")] - Blsttc(#[from] bls::error::Error), - #[error("User name decryption failed")] - UserNameDecryptFailed, - #[error("Using invalid decryption key")] - InvalidDecryptionKey, - #[error("User name encryption failed")] - DiscordNameCipherTooBig, -} diff --git a/sn_transfers/src/genesis.rs b/sn_transfers/src/genesis.rs deleted file mode 100644 index 56d96f5990..0000000000 --- a/sn_transfers/src/genesis.rs +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::wallet::HotWallet; - -use crate::{ - wallet::Result as WalletResult, CashNote, DerivationIndex, MainPubkey, MainSecretKey, - NanoTokens, SignedSpend, Spend, SpendReason, TransferError, UniquePubkey, -}; - -use bls::SecretKey; -use lazy_static::lazy_static; -use std::{ - collections::{BTreeMap, BTreeSet}, - fmt::Debug, - path::PathBuf, -}; -use thiserror::Error; - -/// Number of tokens in the Genesis CashNote. -/// At the inception of the Network 30 % of total supply - i.e. 1,288,490,189 - whole tokens will be created. -/// Each whole token can be subdivided 10^9 times, -/// thus creating a total of 1,288,490,189,000,000,000 available units. -pub(super) const GENESIS_CASHNOTE_AMOUNT: u64 = (0.3 * TOTAL_SUPPLY as f64) as u64; - -/// The input derivation index for the genesis Spend. -pub const GENESIS_INPUT_DERIVATION_INDEX: DerivationIndex = DerivationIndex([0u8; 32]); -/// The output derivation index for the genesis Spend. -pub const GENESIS_OUTPUT_DERIVATION_INDEX: DerivationIndex = DerivationIndex([1u8; 32]); - -/// Default genesis SK for testing purpose. Be sure to pass the correct `GENESIS_SK` value via env for release. -const DEFAULT_LIVE_GENESIS_SK: &str = - "23746be7fa5df26c3065eb7aa26860981e435c1853cafafe472417bc94f340e9"; // DevSkim: ignore DS173237 - -/// Default genesis PK for testing purposes. Be sure to pass the correct `GENESIS_PK` value via env for release. -const DEFAULT_LIVE_GENESIS_PK: &str = "9934c21469a68415e6b06a435709e16bff6e92bf302aeb0ea9199d2d06a55f1b1a21e155853d3f94ae31f8f313f886ee"; // DevSkim: ignore DS173237 - -/// MIN_STORE_COST is 1, hence to have a MIN_ROYALTY_FEE to avoid zero royalty_fee. -const MIN_ROYALTY_FEE: u64 = 1; - -/// Based on the given store cost, it calculates what's the expected amount to be paid as network royalties. -/// Network royalties fee is expected to be 15% of the payment amount, i.e. 85% of store cost + 15% royalties fees. -pub fn calculate_royalties_fee(store_cost: NanoTokens) -> NanoTokens { - let fees_amount = std::cmp::max( - MIN_ROYALTY_FEE, - ((store_cost.as_nano() as f64 * 0.15) / 0.85) as u64, - ); - // we round down the calculated amount - NanoTokens::from(fees_amount) -} - -/// A specialised `Result` type for genesis crate. -pub(super) type GenesisResult = Result; - -/// Total supply of tokens that will eventually exist in the network: 4,294,967,295 * 10^9 = 4,294,967,295,000,000,000. -pub const TOTAL_SUPPLY: u64 = u32::MAX as u64 * u64::pow(10, 9); - -/// Main error type for the crate. -#[derive(Error, Debug, Clone)] -pub enum Error { - /// Error occurred when creating the Genesis CashNote. - #[error("Genesis CashNote error:: {0}")] - GenesisCashNoteError(String), - /// The cash_note error reason that parsing failed. - #[error("Failed to parse reason: {0}")] - FailedToParseReason(#[from] Box), - - #[error("Failed to perform wallet action: {0}")] - WalletError(String), -} - -lazy_static! { - pub static ref GENESIS_PK: MainPubkey = { - let compile_time_key = option_env!("GENESIS_PK").unwrap_or(DEFAULT_LIVE_GENESIS_PK); - let runtime_key = - std::env::var("GENESIS_PK").unwrap_or_else(|_| compile_time_key.to_string()); - - if runtime_key == DEFAULT_LIVE_GENESIS_PK { - warn!("USING DEFAULT GENESIS SK (9934c2) FOR TESTING PURPOSES! EXPECTING PAIRED SK (23746b) TO BE USED!"); - } else if runtime_key == compile_time_key { - warn!("Using compile-time GENESIS_PK: {}", compile_time_key); - } else { - warn!("Overridden by runtime GENESIS_PK: {}", runtime_key); - } - - match MainPubkey::from_hex(&runtime_key) { - Ok(pk) => { - info!("Genesis PK: {pk:?}"); - pk - } - Err(err) => panic!("Failed to parse genesis PK: {err:?}"), - } - }; -} - -lazy_static! { - /// This is the unique key for the genesis Spend - pub static ref GENESIS_SPEND_UNIQUE_KEY: UniquePubkey = GENESIS_PK.new_unique_pubkey(&GENESIS_OUTPUT_DERIVATION_INDEX); -} - -lazy_static! { - pub static ref GENESIS_SK_STR: String = { - let compile_time_key = option_env!("GENESIS_SK").unwrap_or(DEFAULT_LIVE_GENESIS_SK); - let runtime_key = - std::env::var("GENESIS_SK").unwrap_or_else(|_| compile_time_key.to_string()); - - if runtime_key == DEFAULT_LIVE_GENESIS_SK { - warn!("USING DEFAULT GENESIS SK (23746b) FOR TESTING PURPOSES! EXPECTING PAIRED PK (9934c2) TO BE USED!"); - } else if runtime_key == compile_time_key { - warn!("Using compile-time GENESIS_SK"); - } else { - warn!("Overridden by runtime GENESIS_SK"); - } - - runtime_key - }; -} - -lazy_static! { - /// Load the genesis CashNote. - /// The genesis CashNote is the first CashNote in the network. It is created without - /// a source transaction, as there was nothing before it. - pub static ref GENESIS_CASHNOTE: CashNote = { - match create_first_cash_note_from_key(&get_genesis_sk()) { - Ok(cash_note) => cash_note, - Err(err) => panic!("Failed to create genesis CashNote: {err:?}"), - } - }; -} - -/// Returns genesis SK (normally for testing purpose). -pub fn get_genesis_sk() -> MainSecretKey { - match SecretKey::from_hex(&GENESIS_SK_STR) { - Ok(sk) => MainSecretKey::new(sk), - Err(err) => panic!("Failed to parse genesis SK: {err:?}"), - } -} - -/// Return if provided Spend is genesis spend. -pub fn is_genesis_spend(spend: &SignedSpend) -> bool { - let bytes = spend.spend.to_bytes_for_signing(); - spend.spend.unique_pubkey == *GENESIS_SPEND_UNIQUE_KEY - && GENESIS_SPEND_UNIQUE_KEY.verify(&spend.derived_key_sig, bytes) - && spend.spend.amount() == NanoTokens::from(GENESIS_CASHNOTE_AMOUNT) -} - -pub fn load_genesis_wallet() -> Result { - info!("Loading genesis..."); - if let Ok(wallet) = get_existing_genesis_wallet() { - return Ok(wallet); - } - - let mut genesis_wallet = create_genesis_wallet(); - - info!( - "Depositing genesis CashNote: {:?}", - GENESIS_CASHNOTE.unique_pubkey() - ); - genesis_wallet - .deposit_and_store_to_disk(&vec![GENESIS_CASHNOTE.clone()]) - .map_err(|err| Error::WalletError(err.to_string())) - .expect("Genesis wallet shall be stored successfully."); - - let genesis_balance = genesis_wallet.balance(); - info!("Genesis wallet balance: {genesis_balance}"); - - Ok(genesis_wallet) -} - -fn create_genesis_wallet() -> HotWallet { - let root_dir = get_genesis_dir(); - let wallet_dir = root_dir.join("wallet"); - std::fs::create_dir_all(&wallet_dir).expect("Genesis wallet path to be successfully created."); - - crate::wallet::store_new_keypair(&wallet_dir, &get_genesis_sk(), None) - .expect("Genesis key shall be successfully stored."); - - HotWallet::load_from(&root_dir) - .expect("Faucet wallet (after genesis) shall be created successfully.") -} - -fn get_existing_genesis_wallet() -> WalletResult { - let root_dir = get_genesis_dir(); - - let mut wallet = HotWallet::load_from(&root_dir)?; - wallet.try_load_cash_notes()?; - - Ok(wallet) -} - -/// Create a first CashNote given any key (i.e. not specifically the hard coded genesis key). -/// The derivation index is hard coded to ensure deterministic creation. -/// This is useful in tests. -pub fn create_first_cash_note_from_key( - first_cash_note_key: &MainSecretKey, -) -> GenesisResult { - let main_pubkey = first_cash_note_key.main_pubkey(); - debug!("genesis cashnote main_pubkey: {:?}", main_pubkey); - let input_sk = first_cash_note_key.derive_key(&GENESIS_INPUT_DERIVATION_INDEX); - let input_pk = input_sk.unique_pubkey(); - let output_pk = main_pubkey.new_unique_pubkey(&GENESIS_OUTPUT_DERIVATION_INDEX); - let amount = NanoTokens::from(GENESIS_CASHNOTE_AMOUNT); - - let pre_genesis_spend = Spend { - unique_pubkey: input_pk, - reason: SpendReason::default(), - ancestors: BTreeSet::new(), - descendants: BTreeMap::from_iter([(output_pk, amount)]), - royalties: vec![], - }; - let parent_spends = BTreeSet::from_iter([SignedSpend::sign(pre_genesis_spend, &input_sk)]); - - let genesis_cash_note = CashNote { - parent_spends, - main_pubkey, - derivation_index: GENESIS_OUTPUT_DERIVATION_INDEX, - }; - - Ok(genesis_cash_note) -} - -// We need deterministic and fix path for the faucet wallet. -// Otherwise the test instances will not be able to find the same faucet instance. -pub fn get_faucet_data_dir() -> PathBuf { - let mut data_dirs = dirs_next::data_dir().expect("A homedir to exist."); - data_dirs.push("safe"); - data_dirs.push("test_faucet"); - std::fs::create_dir_all(data_dirs.as_path()) - .expect("Faucet test path to be successfully created."); - data_dirs -} - -// We need deterministic and fix path for the genesis wallet. -// Otherwise the test instances will not be able to find the same genesis instance. -fn get_genesis_dir() -> PathBuf { - let mut data_dirs = dirs_next::data_dir().expect("A homedir to exist."); - data_dirs.push("safe"); - data_dirs.push("test_genesis"); - std::fs::create_dir_all(data_dirs.as_path()) - .expect("Genesis test path to be successfully created."); - data_dirs -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn generate_genesis() { - for _ in 0..10 { - let sk = bls::SecretKey::random(); - let sk_str = sk.to_hex(); - let genesis_sk = MainSecretKey::new(sk); - let main_pubkey = genesis_sk.main_pubkey(); - - let genesis_cn = match create_first_cash_note_from_key(&genesis_sk) { - Ok(cash_note) => cash_note, - Err(err) => panic!("Failed to create genesis CashNote: {err:?}"), - }; - - println!("============================="); - println!("secret_key: {sk_str:?}"); - println!("main_pub_key: {:?}", main_pubkey.to_hex()); - println!( - "genesis_cn.unique_pubkey: {:?}", - genesis_cn.unique_pubkey().to_hex() - ); - } - } -} diff --git a/sn_transfers/src/lib.rs b/sn_transfers/src/lib.rs deleted file mode 100644 index 5ea6cbd789..0000000000 --- a/sn_transfers/src/lib.rs +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -#[macro_use] -extern crate tracing; - -mod cashnotes; -mod error; -mod genesis; -mod transfers; -mod wallet; - -/// Types used in the public API -pub use cashnotes::{ - CashNote, DerivationIndex, DerivedSecretKey, Hash, MainPubkey, MainSecretKey, NanoTokens, - SignedSpend, Spend, SpendAddress, SpendReason, UniquePubkey, -}; -pub use error::{Result, TransferError}; -/// Utilities exposed -pub use genesis::{ - calculate_royalties_fee, create_first_cash_note_from_key, get_faucet_data_dir, get_genesis_sk, - is_genesis_spend, load_genesis_wallet, Error as GenesisError, GENESIS_CASHNOTE, - GENESIS_INPUT_DERIVATION_INDEX, GENESIS_OUTPUT_DERIVATION_INDEX, GENESIS_PK, - GENESIS_SPEND_UNIQUE_KEY, TOTAL_SUPPLY, -}; -pub use transfers::{CashNoteRedemption, SignedTransaction, Transfer, UnsignedTransaction}; -pub use wallet::{ - bls_secret_from_hex, wallet_lockfile_name, Error as WalletError, HotWallet, Payment, - PaymentQuote, QuotingMetrics, Result as WalletResult, WalletApi, WatchOnlyWallet, - QUOTE_EXPIRATION_SECS, WALLET_DIR_NAME, -}; - -use bls::SecretKey; -use lazy_static::lazy_static; - -/// The following PKs shall be updated to match its correspondent SKs before the formal release -/// -/// Foundation wallet public key (used to receive initial disbursment from the genesis wallet) -const DEFAULT_FOUNDATION_PK_STR: &str = "8f73b97377f30bed96df1c92daf9f21b4a82c862615439fab8095e68860a5d0dff9f97dba5aef503a26c065e5cb3c7ca"; // DevSkim: ignore DS173237 -/// Public key where network royalties payments are expected to be made to. -const DEFAULT_NETWORK_ROYALTIES_STR: &str = "b4243ec9ceaec374ef992684cd911b209758c5de53d1e406b395bc37ebc8ce50e68755ea6d32da480ae927e1af4ddadb"; // DevSkim: ignore DS173237 -/// Public key where payment forward to be targeted. -const DEFAULT_PAYMENT_FORWARD_STR: &str = "a585839f0502713a0ed6a327f3bd0c301f9e8fe298c93dd00ed7869d8e6804244f0d3014e90df45cd344a7ccd702865c"; // DevSkim: ignore DS173237 -/// Default secrect key where payment forward to be targeted, for backward compatible purpose only. -const DEFAULT_PAYMENT_FORWARD_SK_STR: &str = - "49113d2083f57a976076adbe85decb75115820de1e6e74b47e0429338cef124a"; // DevSkim: ignore DS173237 - -lazy_static! { - pub static ref FOUNDATION_PK: MainPubkey = { - let compile_time_key = option_env!("FOUNDATION_PK").unwrap_or(DEFAULT_FOUNDATION_PK_STR); - let runtime_key = - std::env::var("FOUNDATION_PK").unwrap_or_else(|_| compile_time_key.to_string()); - - if runtime_key == DEFAULT_FOUNDATION_PK_STR { - warn!("Using default FOUNDATION_PK: {}", DEFAULT_FOUNDATION_PK_STR); - } else if runtime_key == compile_time_key { - warn!("Using compile-time FOUNDATION_PK: {}", compile_time_key); - } else { - warn!("Overridden by runtime FOUNDATION_PK: {}", runtime_key); - } - - match MainPubkey::from_hex(&runtime_key) { - Ok(pk) => pk, - Err(err) => panic!("Failed to parse foundation PK: {err:?}"), - } - }; -} - -lazy_static! { - pub static ref NETWORK_ROYALTIES_PK: MainPubkey = { - let compile_time_key = - option_env!("NETWORK_ROYALTIES_PK").unwrap_or(DEFAULT_NETWORK_ROYALTIES_STR); - let runtime_key = - std::env::var("NETWORK_ROYALTIES_PK").unwrap_or_else(|_| compile_time_key.to_string()); - - if runtime_key == DEFAULT_NETWORK_ROYALTIES_STR { - warn!( - "Using default NETWORK_ROYALTIES_PK: {}", - DEFAULT_NETWORK_ROYALTIES_STR - ); - } else if runtime_key == compile_time_key { - warn!( - "Using compile-time NETWORK_ROYALTIES_PK: {}", - compile_time_key - ); - } else { - warn!( - "Overridden by runtime NETWORK_ROYALTIES_PK: {}", - runtime_key - ); - } - - match MainPubkey::from_hex(&runtime_key) { - Ok(pk) => pk, - Err(err) => panic!("Failed to parse network royalties PK: {err:?}"), - } - }; - pub static ref DEFAULT_NETWORK_ROYALTIES_PK: MainPubkey = { - match MainPubkey::from_hex(DEFAULT_NETWORK_ROYALTIES_STR) { - Ok(pk) => pk, - Err(err) => panic!("Failed to parse default network royalties PK: {err:?}"), - } - }; -} - -lazy_static! { - pub static ref PAYMENT_FORWARD_PK: MainPubkey = { - let compile_time_key = - option_env!("PAYMENT_FORWARD_PK").unwrap_or(DEFAULT_PAYMENT_FORWARD_STR); - let runtime_key = - std::env::var("PAYMENT_FORWARD_PK").unwrap_or_else(|_| compile_time_key.to_string()); - - if runtime_key == DEFAULT_PAYMENT_FORWARD_STR { - warn!( - "Using default PAYMENT_FORWARD_PK: {}", - DEFAULT_PAYMENT_FORWARD_STR - ); - } else if runtime_key == compile_time_key { - warn!( - "Using compile-time PAYMENT_FORWARD_PK: {}", - compile_time_key - ); - } else { - warn!("Overridden by runtime PAYMENT_FORWARD_PK: {}", runtime_key); - } - - match MainPubkey::from_hex(&runtime_key) { - Ok(pk) => pk, - Err(err) => panic!("Failed to parse payment forward PK: {err:?}"), - } - }; - pub static ref DEFAULT_PAYMENT_FORWARD_SK: SecretKey = { - match SecretKey::from_hex(DEFAULT_PAYMENT_FORWARD_SK_STR) { - Ok(sk) => sk, - Err(err) => panic!("Failed to parse default payment forward SK: {err:?}"), - } - }; -} - -// re-export crates used in our public API -pub use bls::{self, rand, Ciphertext, Signature}; - -/// This is a helper module to make it a bit easier -/// and regular for API callers to instantiate -/// an Rng when calling sn_transfers methods that require -/// them. -pub mod rng { - use crate::rand::{ - rngs::{StdRng, ThreadRng}, - SeedableRng, - }; - use tiny_keccak::{Hasher, Sha3}; - - pub fn thread_rng() -> ThreadRng { - crate::rand::thread_rng() - } - - pub fn from_seed(seed: ::Seed) -> StdRng { - StdRng::from_seed(seed) - } - - // Using hash to covert `Vec` into `[u8; 32]', - // and using it as seed to generate a determined Rng. - pub fn from_vec(vec: &[u8]) -> StdRng { - let mut sha3 = Sha3::v256(); - sha3.update(vec); - let mut hash = [0u8; 32]; - sha3.finalize(&mut hash); - - from_seed(hash) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::rng::from_vec; - - #[test] - fn confirm_generating_same_key() { - let rng_seed = b"testing generating same key"; - let content = b"some context to try with"; - - let mut rng_1 = from_vec(rng_seed); - let reward_key_1 = MainSecretKey::random_from_rng(&mut rng_1); - let sig = reward_key_1.sign(content); - - let mut rng_2 = from_vec(rng_seed); - let reward_key_2 = MainSecretKey::random_from_rng(&mut rng_2); - - assert!(reward_key_2.main_pubkey().verify(&sig, content)); - } -} diff --git a/sn_transfers/src/transfers.rs b/sn_transfers/src/transfers.rs deleted file mode 100644 index e73d239897..0000000000 --- a/sn_transfers/src/transfers.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -mod signed_transaction; -mod transfer; -mod unsigned_transaction; - -pub use signed_transaction::SignedTransaction; -pub use transfer::{CashNoteRedemption, Transfer}; -pub use unsigned_transaction::UnsignedTransaction; diff --git a/sn_transfers/src/transfers/signed_transaction.rs b/sn_transfers/src/transfers/signed_transaction.rs deleted file mode 100644 index b69a70f5ae..0000000000 --- a/sn_transfers/src/transfers/signed_transaction.rs +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use std::collections::BTreeSet; - -use crate::error::Result; -use crate::{ - CashNote, DerivationIndex, MainPubkey, MainSecretKey, NanoTokens, SignedSpend, SpendReason, - TransferError, UnsignedTransaction, -}; -use serde::{Deserialize, Serialize}; - -/// A local transaction that has been signed and is ready to be executed on the Network -#[derive(custom_debug::Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct SignedTransaction { - /// Output CashNotes ready to be packaged into a `Transfer` - #[debug(skip)] - pub output_cashnotes: Vec, - /// Change CashNote ready to be added back to our wallet - #[debug(skip)] - pub change_cashnote: Option, - /// All the spends ready to be sent to the Network - pub spends: BTreeSet, -} - -impl SignedTransaction { - /// Create a new `SignedTransaction` - /// - `available_cash_notes`: provide the available cash notes assumed to be not spent yet - /// - `recipients`: recipient amounts, mainpubkey, the random derivation index to use, and whether it is royalty fee - /// - `change_to`: what mainpubkey to give the change to - /// - `input_reason_hash`: an optional `SpendReason` - /// - `main_key`: the main secret key that owns the available cash notes, used for signature - pub fn new( - available_cash_notes: Vec, - recipients: Vec<(NanoTokens, MainPubkey, DerivationIndex, bool)>, - change_to: MainPubkey, - input_reason_hash: SpendReason, - main_key: &MainSecretKey, - ) -> Result { - let unsigned_tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - )?; - let signed_tx = unsigned_tx.sign(main_key)?; - Ok(signed_tx) - } - - /// Verify the `SignedTransaction` - pub fn verify(&self) -> Result<()> { - for cn in self.output_cashnotes.iter() { - cn.verify()?; - } - if let Some(ref cn) = self.change_cashnote { - cn.verify()?; - } - for spend in self.spends.iter() { - spend.verify()?; - } - Ok(()) - } - - /// Create a new `SignedTransaction` from a hex string - pub fn from_hex(hex: &str) -> Result { - let decoded_hex = hex::decode(hex).map_err(|e| { - TransferError::TransactionSerialization(format!("Hex decode failed: {e}")) - })?; - let s = rmp_serde::from_slice(&decoded_hex).map_err(|e| { - TransferError::TransactionSerialization(format!("Failed to deserialize: {e}")) - })?; - Ok(s) - } - - /// Return the hex representation of the `SignedTransaction` - pub fn to_hex(&self) -> Result { - Ok(hex::encode(rmp_serde::to_vec(self).map_err(|e| { - TransferError::TransactionSerialization(format!("Failed to serialize: {e}")) - })?)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_unsigned_tx_serialization() -> Result<()> { - let mut rng = rand::thread_rng(); - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 100); - - let available_cash_notes = vec![CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }]; - let recipients = vec![ - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - let signed_tx = tx.sign(&cnr_sk).expect("Sign to succeed"); - - let hex = signed_tx.to_hex()?; - let signed_tx2 = SignedTransaction::from_hex(&hex)?; - - assert_eq!(signed_tx, signed_tx2); - Ok(()) - } - - #[test] - fn test_unsigned_tx_verify_simple() -> Result<()> { - let mut rng = rand::thread_rng(); - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 100); - - let available_cash_notes = vec![CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }]; - let recipients = vec![ - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - let signed_tx = tx.sign(&cnr_sk).expect("Sign to succeed"); - - let res = signed_tx.verify(); - assert_eq!(res, Ok(())); - Ok(()) - } -} diff --git a/sn_transfers/src/transfers/transfer.rs b/sn_transfers/src/transfers/transfer.rs deleted file mode 100644 index 7c89826472..0000000000 --- a/sn_transfers/src/transfers/transfer.rs +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{CashNote, Ciphertext, DerivationIndex, MainPubkey, MainSecretKey, SpendAddress}; - -use rayon::iter::ParallelIterator; -use rayon::prelude::IntoParallelRefIterator; - -use serde::{Deserialize, Serialize}; -use std::collections::hash_map::DefaultHasher; -use std::collections::BTreeSet; -use std::hash::{Hash, Hasher}; - -use crate::error::{Result, TransferError}; - -/// Transfer sent to a recipient -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Hash)] -pub enum Transfer { - /// List of encrypted CashNoteRedemptions from which a recipient can verify and get money - /// Only the recipient can decrypt these CashNoteRedemptions - Encrypted(Vec), - /// The network requires a payment as network royalties for storage which nodes can validate - /// and verify, these CashNoteRedemptions need to be sent to storage nodes as payment proof as well. - NetworkRoyalties(Vec), -} - -impl std::fmt::Debug for Transfer { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::NetworkRoyalties(cn_redemptions) => { - write!(f, "Transfer::NetworkRoyalties: {cn_redemptions:?}") - } - Self::Encrypted(transfers) => { - // Iterate over the transfers and log the hash of each encrypted transfer - let hashed: Vec<_> = transfers - .iter() - .map(|transfer| { - // Calculate the hash of the transfer - let mut hasher = DefaultHasher::new(); - transfer.hash(&mut hasher); - hasher.finish() - }) - .collect(); - // Write the encrypted transfers to the formatter - write!(f, "Transfer::Encrypted: {hashed:?}") - } - } - } -} - -impl Transfer { - /// This function is used to create a Transfer from a CashNote, can be done offline, and sent to the recipient. - /// Creates a Transfer from the given cash_note - /// This Transfer can be sent safely to the recipients as all data in it is encrypted - /// The recipients can then decrypt the data and use it to verify and reconstruct the CashNote - pub fn transfer_from_cash_note(cash_note: &CashNote) -> Result { - let recipient = cash_note.main_pubkey; - let u = CashNoteRedemption::from_cash_note(cash_note); - let t = Self::create(vec![u], recipient) - .map_err(|_| TransferError::CashNoteRedemptionEncryptionFailed)?; - Ok(t) - } - - /// This function is used to create a Network Royalties Transfer from a CashNote - /// can be done offline, and sent to the recipient. - /// Note that this type of transfer is not encrypted - pub(crate) fn royalties_transfer_from_cash_note(cash_note: &CashNote) -> Result { - let cnr = CashNoteRedemption::from_cash_note(cash_note); - Ok(Self::NetworkRoyalties(vec![cnr])) - } - - /// Create a new transfer - /// cashnote_redemptions: List of CashNoteRedemptions to be used for payment - /// recipient: main Public key (donation key) of the recipient, - /// not to be confused with the derived keys - pub fn create( - cashnote_redemptions: Vec, - recipient: MainPubkey, - ) -> Result { - let encrypted_cashnote_redemptions = cashnote_redemptions - .into_iter() - .map(|cashnote_redemption| cashnote_redemption.encrypt(recipient)) - .collect::>>()?; - Ok(Self::Encrypted(encrypted_cashnote_redemptions)) - } - - /// Get the CashNoteRedemptions from the Payment - /// This is used by the recipient of a payment to decrypt the cashnote_redemptions in a payment - pub fn cashnote_redemptions(&self, sk: &MainSecretKey) -> Result> { - match self { - Self::Encrypted(cyphers) => { - let cashnote_redemptions: Result> = cyphers - .par_iter() // Use Rayon's par_iter for parallel processing - .map(|cypher| CashNoteRedemption::decrypt(cypher, sk)) // Decrypt each CashNoteRedemption - .collect(); // Collect results into a vector - let cashnote_redemptions = cashnote_redemptions?; // Propagate error if any - Ok(cashnote_redemptions) - } - Self::NetworkRoyalties(cnr) => Ok(cnr.clone()), - } - } - - /// Deserializes a `Transfer` represented as a hex string to a `Transfer`. - pub fn from_hex(hex: &str) -> Result { - let mut bytes = - hex::decode(hex).map_err(|_| TransferError::TransferDeserializationFailed)?; - bytes.reverse(); - let transfer: Self = rmp_serde::from_slice(&bytes) - .map_err(|_| TransferError::TransferDeserializationFailed)?; - Ok(transfer) - } - - /// Serialize this `Transfer` instance to a readable hex string that a human can copy paste - pub fn to_hex(&self) -> Result { - let mut serialized = - rmp_serde::to_vec(&self).map_err(|_| TransferError::TransferSerializationFailed)?; - serialized.reverse(); - Ok(hex::encode(serialized)) - } -} - -/// Unspent Transaction (Tx) Output -/// Information can be used by the Tx recipient of this output -/// to check that they received money and to spend it -/// -/// This struct contains sensitive information that should be kept secret -/// so it should be encrypted to the recipient's public key (public address) -#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, Debug, Hash)] -pub struct CashNoteRedemption { - /// derivation index of the CashNoteRedemption - /// with this derivation index the owner can derive - /// the secret key from their main key needed to spend this CashNoteRedemption - pub derivation_index: DerivationIndex, - /// address of parent spends - /// using data found at these addresses the owner can check that the output is valid money - pub parent_spends: BTreeSet, -} - -impl CashNoteRedemption { - /// Create a new CashNoteRedemption - pub fn new(derivation_index: DerivationIndex, parent_spends: BTreeSet) -> Self { - Self { - derivation_index, - parent_spends, - } - } - - pub fn from_cash_note(cash_note: &CashNote) -> Self { - let derivation_index = cash_note.derivation_index(); - let parent_spends = cash_note - .parent_spends - .iter() - .map(|s| s.address()) - .collect(); - Self::new(derivation_index, parent_spends) - } - - /// Serialize the CashNoteRedemption to bytes - pub fn to_bytes(&self) -> Result> { - rmp_serde::to_vec(self).map_err(|_| TransferError::CashNoteRedemptionSerialisationFailed) - } - - /// Deserialize the CashNoteRedemption from bytes - pub fn from_bytes(bytes: &[u8]) -> Result { - rmp_serde::from_slice(bytes) - .map_err(|_| TransferError::CashNoteRedemptionSerialisationFailed) - } - - /// Encrypt the CashNoteRedemption to a public key - pub fn encrypt(&self, pk: MainPubkey) -> Result { - let bytes = self.to_bytes()?; - Ok(pk.0.encrypt(bytes)) - } - - /// Decrypt the CashNoteRedemption with a secret key - pub fn decrypt(cypher: &Ciphertext, sk: &MainSecretKey) -> Result { - let bytes = sk - .secret_key() - .decrypt(cypher) - .ok_or(TransferError::CashNoteRedemptionDecryptionFailed)?; - Self::from_bytes(&bytes) - } -} - -#[cfg(test)] -mod tests { - use xor_name::XorName; - - use super::*; - - #[test] - fn test_cashnote_redemption_conversions() { - let rng = &mut bls::rand::thread_rng(); - let cashnote_redemption = CashNoteRedemption::new( - DerivationIndex([42; 32]), - BTreeSet::from_iter([SpendAddress::new(XorName::random(rng))]), - ); - let sk = MainSecretKey::random(); - let pk = sk.main_pubkey(); - - let bytes = cashnote_redemption.to_bytes().unwrap(); - let cipher = cashnote_redemption.encrypt(pk).unwrap(); - - let cashnote_redemption2 = CashNoteRedemption::from_bytes(&bytes).unwrap(); - let cashnote_redemption3 = CashNoteRedemption::decrypt(&cipher, &sk).unwrap(); - - assert_eq!(cashnote_redemption, cashnote_redemption2); - assert_eq!(cashnote_redemption, cashnote_redemption3); - } - - #[test] - fn test_cashnote_redemption_transfer() { - let rng = &mut bls::rand::thread_rng(); - let cashnote_redemption = CashNoteRedemption::new( - DerivationIndex([42; 32]), - BTreeSet::from_iter([SpendAddress::new(XorName::random(rng))]), - ); - let sk = MainSecretKey::random(); - let pk = sk.main_pubkey(); - - let payment = Transfer::create(vec![cashnote_redemption.clone()], pk).unwrap(); - let cashnote_redemptions = payment.cashnote_redemptions(&sk).unwrap(); - - assert_eq!(cashnote_redemptions, vec![cashnote_redemption]); - } -} diff --git a/sn_transfers/src/transfers/unsigned_transaction.rs b/sn_transfers/src/transfers/unsigned_transaction.rs deleted file mode 100644 index 060de4b3e5..0000000000 --- a/sn_transfers/src/transfers/unsigned_transaction.rs +++ /dev/null @@ -1,1128 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use std::cmp::min; -use std::collections::{BTreeMap, BTreeSet}; -use std::fmt::Debug; - -use crate::UniquePubkey; -use crate::{ - error::Result, CashNote, DerivationIndex, MainPubkey, MainSecretKey, NanoTokens, SignedSpend, - SignedTransaction, Spend, SpendReason, TransferError, -}; - -use serde::{Deserialize, Serialize}; - -/// A local transaction that has not been signed yet -/// All fields are private to prevent bad useage -#[derive(Clone, Serialize, Deserialize, PartialEq)] -pub struct UnsignedTransaction { - /// Output CashNotes stripped of their parent spends, unuseable as is - output_cashnotes_without_spends: Vec, - /// Change CashNote stripped of its parent spends, unuseable as is - pub change_cashnote_without_spends: Option, - /// Spends waiting to be signed along with their secret derivation index - spends: Vec<(Spend, DerivationIndex)>, -} - -impl Debug for UnsignedTransaction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("UnsignedTransaction") - .field( - "spends", - &self.spends.iter().map(|(s, _)| s).collect::>(), - ) - .finish() - } -} - -impl UnsignedTransaction { - /// Create a new `UnsignedTransaction` with the given inputs and outputs - /// This function will perform a distribution of the input value to the outputs - /// In the figure below, inputs and outputs represent `CashNote`s, - /// which are spent thus creating spends that commit to a transfer of value to the outputs. - /// The value of the outputs is the sum of the values given to them by the inputs. - /// - /// ```text - /// - /// inputA(7) inputB(5) - /// | | - /// | | - /// spend1 spend2 - /// / \ / \ \__________ - /// 5 2 2 1 2 - /// / \ / \ \ - /// outputA(5) outputB(4) outputC(1) change(2) - /// - /// ``` - /// - /// Once created, the `UnsignedTransaction` can be signed with the owner's `MainSecretKey` using the `sign` method - pub fn new( - available_cash_notes: Vec, - recipients: Vec<(NanoTokens, MainPubkey, DerivationIndex, bool)>, - change_to: MainPubkey, - input_reason_hash: SpendReason, - ) -> Result { - // check output amounts (reject zeroes and overflowing values) - let total_output_amount = recipients - .iter() - .try_fold(NanoTokens::zero(), |total, (amount, _, _, _)| { - total.checked_add(*amount) - }) - .ok_or(TransferError::ExcessiveNanoValue)?; - if total_output_amount == NanoTokens::zero() - || recipients - .iter() - .any(|(amount, _, _, _)| amount.as_nano() == 0) - { - return Err(TransferError::ZeroOutputs); - } - - // check input amounts - let total_input_amount = available_cash_notes - .iter() - .map(|cn| cn.value()) - .try_fold(NanoTokens::zero(), |total, amount| { - total.checked_add(amount) - }) - .ok_or(TransferError::ExcessiveNanoValue)?; - if total_output_amount > total_input_amount { - return Err(TransferError::NotEnoughBalance( - total_input_amount, - total_output_amount, - )); - } - - // create empty output cash notes for recipients - let outputs: Vec<(CashNote, NanoTokens, bool)> = recipients - .iter() - .map(|(amount, main_pk, derivation_index, is_royaltiy)| { - let cn = CashNote { - parent_spends: BTreeSet::new(), - main_pubkey: *main_pk, - derivation_index: *derivation_index, - }; - (cn, *amount, *is_royaltiy) - }) - .collect(); - - // order inputs by value, re const after sorting - let mut cashnotes_big_to_small = available_cash_notes; - cashnotes_big_to_small.sort_by_key(|b| std::cmp::Reverse(b.value())); - let cashnotes_big_to_small = cashnotes_big_to_small; - - // distribute value from inputs to output cash notes - let mut spends = Vec::new(); - let mut change_cn = None; - let mut outputs_iter = outputs.iter(); - let mut current_output = outputs_iter.next(); - let mut current_output_remaining_value = current_output - .map(|(_, amount, _)| amount.as_nano()) - .unwrap_or(0); - let mut no_more_outputs = false; - for input in cashnotes_big_to_small { - let input_key = input.unique_pubkey(); - let input_value = input.value(); - let input_ancestors = input - .parent_spends - .iter() - .map(|s| *s.unique_pubkey()) - .collect(); - let mut input_remaining_value = input_value.as_nano(); - let mut donate_to = BTreeMap::new(); - let mut royalties = vec![]; - - // take value from input and distribute it to outputs - while input_remaining_value > 0 { - if let Some((output, _, is_royalty)) = current_output { - // give as much as possible to the current output - let amount_to_take = min(input_remaining_value, current_output_remaining_value); - input_remaining_value -= amount_to_take; - current_output_remaining_value -= amount_to_take; - let output_key = output.unique_pubkey(); - donate_to.insert(output_key, NanoTokens::from(amount_to_take)); - if *is_royalty { - royalties.push(output.derivation_index); - } - - // move to the next output if the current one is fully funded - if current_output_remaining_value == 0 { - current_output = outputs_iter.next(); - current_output_remaining_value = current_output - .map(|(_, amount, _)| amount.as_nano()) - .unwrap_or(0); - } - } else { - // if we run out of outputs, send the rest as change - let rng = &mut rand::thread_rng(); - let change_derivation_index = DerivationIndex::random(rng); - let change_key = change_to.new_unique_pubkey(&change_derivation_index); - donate_to.insert(change_key, NanoTokens::from(input_remaining_value)); - - // assign the change cash note - change_cn = Some(CashNote { - parent_spends: BTreeSet::new(), - main_pubkey: change_to, - derivation_index: change_derivation_index, - }); - let change_amount = NanoTokens::from(input_remaining_value); - donate_to.insert(change_key, change_amount); - no_more_outputs = true; - break; - } - } - - // build spend with donations computed above - let spend = Spend { - unique_pubkey: input_key, - ancestors: input_ancestors, - descendants: donate_to, - reason: input_reason_hash.clone(), - royalties, - }; - spends.push((spend, input.derivation_index)); - - // if we run out of outputs, we don't need to use all the inputs - if no_more_outputs { - break; - } - } - - // return the UnsignedTransaction - let output_cashnotes_without_spends = outputs.into_iter().map(|(cn, _, _)| cn).collect(); - Ok(Self { - output_cashnotes_without_spends, - change_cashnote_without_spends: change_cn, - spends, - }) - } - - /// Sign the `UnsignedTransaction` with the given secret key - /// and return the `SignedTransaction` - /// It is advised to verify the `UnsignedTransaction` before signing if it comes from an external source - pub fn sign(self, sk: &MainSecretKey) -> Result { - // sign the spends - let signed_spends: BTreeSet = self - .spends - .iter() - .map(|(spend, derivation_index)| { - let derived_sk = sk.derive_key(derivation_index); - SignedSpend::sign(spend.clone(), &derived_sk) - }) - .collect(); - - // distribute signed spends to their respective CashNotes - let change_cashnote = self.change_cashnote_without_spends.map(|mut cn| { - let us = cn.unique_pubkey(); - let parent_spends = signed_spends - .iter() - .filter(|ss| ss.spend.descendants.keys().any(|k| k == &us)) - .cloned() - .collect(); - cn.parent_spends = parent_spends; - cn - }); - let output_cashnotes = self - .output_cashnotes_without_spends - .into_iter() - .map(|mut cn| { - let us = cn.unique_pubkey(); - let parent_spends = signed_spends - .iter() - .filter(|ss| ss.spend.descendants.keys().any(|k| k == &us)) - .cloned() - .collect(); - cn.parent_spends = parent_spends; - cn - }) - .collect(); - - Ok(SignedTransaction { - output_cashnotes, - change_cashnote, - spends: signed_spends, - }) - } - - /// Verify the `UnsignedTransaction` - pub fn verify(&self) -> Result<()> { - // verify that the tx is balanced - let input_sum: u64 = self - .spends - .iter() - .map(|(spend, _)| spend.amount().as_nano()) - .sum(); - let output_sum: u64 = self - .output_cashnotes_without_spends - .iter() - .chain(self.change_cashnote_without_spends.iter()) - .map(|cn| cn.value().as_nano()) - .sum(); - if input_sum != output_sum { - return Err(TransferError::InvalidUnsignedTransaction(format!( - "Unbalanced transaction: input sum: {input_sum} != output sum {output_sum}" - ))); - } - - // verify that all spends have a unique pubkey - let mut unique_pubkeys = BTreeSet::new(); - for (spend, _) in &self.spends { - let u = spend.unique_pubkey; - if !unique_pubkeys.insert(u) { - return Err(TransferError::InvalidUnsignedTransaction(format!( - "Spends are not unique in this transaction, there are multiple spends for: {u}" - ))); - } - } - - // verify that all cash notes have a unique pubkey, distinct from spends - for cn in self - .output_cashnotes_without_spends - .iter() - .chain(self.change_cashnote_without_spends.iter()) - { - let u = cn.unique_pubkey(); - if !unique_pubkeys.insert(u) { - return Err(TransferError::InvalidUnsignedTransaction( - format!("Cash note unique pubkeys are not unique in this transaction, there are multiple outputs for: {u}"), - )); - } - } - - // verify that spends refer to the outputs and that the amounts match - let mut amounts_by_unique_pubkey = BTreeMap::new(); - for (spend, _) in &self.spends { - for (k, v) in &spend.descendants { - amounts_by_unique_pubkey - .entry(*k) - .and_modify(|sum| *sum += v.as_nano()) - .or_insert(v.as_nano()); - } - } - for cn in self - .output_cashnotes_without_spends - .iter() - .chain(self.change_cashnote_without_spends.iter()) - { - let u = cn.unique_pubkey(); - let expected_amount = amounts_by_unique_pubkey.get(&u).copied().unwrap_or(0); - let amount = cn.value().as_nano(); - if expected_amount != amount { - return Err(TransferError::InvalidUnsignedTransaction( - format!("Invalid amount for CashNote: {u} has {expected_amount} acording to spends but self reports {amount}"), - )); - } - } - Ok(()) - } - - /// Return the unique keys of the CashNotes that have been spent along with their amounts - pub fn spent_unique_keys(&self) -> BTreeSet<(UniquePubkey, NanoTokens)> { - self.spends - .iter() - .map(|(spend, _)| (spend.unique_pubkey, spend.amount())) - .collect() - } - - /// Return the unique keys of the CashNotes that have been created along with their amounts - pub fn output_unique_keys(&self) -> BTreeSet<(UniquePubkey, NanoTokens)> { - self.spends - .iter() - .flat_map(|(spend, _)| spend.descendants.iter().map(|(k, v)| (*k, *v))) - .collect() - } - - /// Create a new `UnsignedTransaction` from a hex string - pub fn from_hex(hex: &str) -> Result { - let decoded_hex = hex::decode(hex).map_err(|e| { - TransferError::TransactionSerialization(format!("Hex decode failed: {e}")) - })?; - let s = rmp_serde::from_slice(&decoded_hex).map_err(|e| { - TransferError::TransactionSerialization(format!("Failed to deserialize: {e}")) - })?; - Ok(s) - } - - /// Return the hex representation of the `UnsignedTransaction` - pub fn to_hex(&self) -> Result { - Ok(hex::encode(rmp_serde::to_vec(self).map_err(|e| { - TransferError::TransactionSerialization(format!("Failed to serialize: {e}")) - })?)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use eyre::{Ok, Result}; - - #[test] - fn test_unsigned_tx_serialization() -> Result<()> { - let mut rng = rand::thread_rng(); - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 100); - - let available_cash_notes = vec![CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }]; - let recipients = vec![ - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - let hex = tx.to_hex()?; - let tx2 = UnsignedTransaction::from_hex(&hex)?; - - assert_eq!(tx, tx2); - Ok(()) - } - - #[test] - fn test_unsigned_tx_empty_inputs_is_rejected() -> Result<()> { - let mut rng = rand::thread_rng(); - let available_cash_notes = vec![]; - let recipients = vec![ - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ); - assert_eq!( - tx, - Err(TransferError::NotEnoughBalance( - NanoTokens::zero(), - NanoTokens::from(2) - )) - ); - Ok(()) - } - - #[test] - fn test_unsigned_tx_empty_outputs_is_rejected() -> Result<()> { - let mut rng = rand::thread_rng(); - let available_cash_notes = vec![CashNote { - parent_spends: BTreeSet::new(), - main_pubkey: MainSecretKey::random().main_pubkey(), - derivation_index: DerivationIndex::random(&mut rng), - }]; - let recipients = vec![]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = SpendReason::default(); - let tx = UnsignedTransaction::new( - available_cash_notes.clone(), - recipients, - change_to, - input_reason_hash.clone(), - ); - assert_eq!(tx, Err(TransferError::ZeroOutputs)); - let recipients = vec![( - NanoTokens::zero(), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - )]; - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ); - assert_eq!(tx, Err(TransferError::ZeroOutputs)); - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_insufficient_funds() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 100 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 100); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 100 -> 50 + 55 - let available_cash_notes = vec![cn1]; - let recipients = vec![ - ( - NanoTokens::from(50), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(55), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ); - - assert_eq!( - tx, - Err(TransferError::NotEnoughBalance( - NanoTokens::from(100), - NanoTokens::from(105) - )) - ); - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_1_to_2() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 100 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 100); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 100 -> 50 + 25 + 25 change - let available_cash_notes = vec![cn1]; - let recipients = vec![ - ( - NanoTokens::from(50), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(25), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - // sign the transaction - let signed_tx = tx.sign(&cnr_sk).expect("signing to succeed"); - - // verify the transaction - signed_tx.verify().expect("verify to succeed"); - - // check the output cash notes - let output_values: BTreeSet = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .collect(); - assert_eq!(output_values, BTreeSet::from_iter([50, 25])); - assert_eq!( - signed_tx - .change_cashnote - .as_ref() - .expect("to have a change cashnote") - .value() - .as_nano(), - 25 - ); - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_2_to_1() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 50 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 50); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 25 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 25); - let cn2 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 50 + 25 -> 75 + 0 change - let available_cash_notes = vec![cn1, cn2]; - let recipients = vec![( - NanoTokens::from(75), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - )]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - // sign the transaction - let signed_tx = tx.sign(&cnr_sk).expect("signing to succeed"); - - // verify the transaction - signed_tx.verify().expect("verify to succeed"); - - // check the output cash notes - let output_values: BTreeSet = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .collect(); - assert_eq!(output_values, BTreeSet::from_iter([75])); - assert_eq!(signed_tx.change_cashnote, None); - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_2_to_2() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 50 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 50); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 25 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 25); - let cn2 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 50 + 25 -> 10 + 60 + 5 change - let available_cash_notes = vec![cn1, cn2]; - let recipients = vec![ - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(60), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - // sign the transaction - let signed_tx = tx.sign(&cnr_sk).expect("signing to succeed"); - - // verify the transaction - signed_tx.verify().expect("verify to succeed"); - - // check the output cash notes - let output_values: BTreeSet = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .collect(); - assert_eq!(output_values, BTreeSet::from_iter([10, 60])); - assert_eq!( - signed_tx - .change_cashnote - .as_ref() - .expect("to have a change cashnote") - .value() - .as_nano(), - 5 - ); - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_3_to_2() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 10 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 10); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 20 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 20); - let cn2 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 30 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 30); - let cn3 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 10 + 20 + 30 -> 31 + 21 + 8 change - let available_cash_notes = vec![cn1, cn2, cn3]; - let recipients = vec![ - ( - NanoTokens::from(31), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(21), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - // sign the transaction - let signed_tx = tx.sign(&cnr_sk).expect("signing to succeed"); - - // verify the transaction - signed_tx.verify().expect("verify to succeed"); - - // check the output cash notes - let output_values: BTreeSet = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .collect(); - assert_eq!(output_values, BTreeSet::from_iter([31, 21])); - assert_eq!( - signed_tx - .change_cashnote - .as_ref() - .expect("to have a change cashnote") - .value() - .as_nano(), - 8 - ); - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_3_to_many_use_1() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 10 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 10); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 120 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 120); - let cn2 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 2 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 2); - let cn3 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 10(unused) + 120 + 1(unused) -> 10 + 1 + 10 + 1 + 10 + 1 + 10 + 1 + 10 + 1 + 10 + 1 + 54 change and two unused inputs - let available_cash_notes = vec![cn1, cn2, cn3]; - let recipients = vec![ - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - // sign the transaction - let signed_tx = tx.sign(&cnr_sk).expect("signing to succeed"); - - // verify the transaction - signed_tx.verify().expect("verify to succeed"); - - // check the output cash notes - let output_values: BTreeSet = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .collect(); - assert_eq!( - output_values, - BTreeSet::from_iter([10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1]) - ); - assert_eq!( - signed_tx - .change_cashnote - .as_ref() - .expect("to have a change cashnote") - .value() - .as_nano(), - 54 - ); - assert_eq!(signed_tx.spends.len(), 1); // only used the first input - Ok(()) - } - - #[test] - fn test_unsigned_tx_distribution_3_to_many_use_all() -> Result<()> { - let mut rng = rand::thread_rng(); - - // create an input cash note of 10 - let cnr_sk = MainSecretKey::random(); - let cnr_pk = cnr_sk.main_pubkey(); - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 30); - let cn1 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 2 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 32); - let cn2 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an input cash note of 120 - let cnr_di = DerivationIndex::random(&mut rng); - let cnr_upk = cnr_pk.new_unique_pubkey(&cnr_di); - let spend = SignedSpend::random_spend_to(&mut rng, cnr_upk, 33); - let cn3 = CashNote { - parent_spends: BTreeSet::from_iter([spend]), - main_pubkey: cnr_pk, - derivation_index: cnr_di, - }; - - // create an unsigned transaction - // 30 + 32 + 33 -> 10 + 1 + 10 + 1 + 10 + 1 + 10 + 1 + 10 + 1 + 10 + 1 + 29 change - let available_cash_notes = vec![cn1, cn2, cn3]; - let recipients = vec![ - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ( - NanoTokens::from(10), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - false, - ), - ( - NanoTokens::from(1), - MainSecretKey::random().main_pubkey(), - DerivationIndex::random(&mut rng), - true, - ), - ]; - let change_to = MainSecretKey::random().main_pubkey(); - let input_reason_hash = Default::default(); - let tx = UnsignedTransaction::new( - available_cash_notes, - recipients, - change_to, - input_reason_hash, - ) - .expect("UnsignedTransaction creation to succeed"); - - // sign the transaction - let signed_tx = tx.sign(&cnr_sk).expect("signing to succeed"); - - // verify the transaction - signed_tx.verify().expect("verify to succeed"); - - // check the output cash notes - let output_values: BTreeSet = signed_tx - .output_cashnotes - .iter() - .map(|cn| cn.value().as_nano()) - .collect(); - assert_eq!( - output_values, - BTreeSet::from_iter([10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1]) - ); - assert_eq!( - signed_tx - .change_cashnote - .as_ref() - .expect("to have a change cashnote") - .value() - .as_nano(), - 29 - ); - Ok(()) - } -} diff --git a/sn_transfers/src/wallet.rs b/sn_transfers/src/wallet.rs deleted file mode 100644 index 2a12bfe542..0000000000 --- a/sn_transfers/src/wallet.rs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -//! An implementation of a local Wallet used by clients and nodes (the latter use them for their rewards). -//! There is one which is deposit only, and one which can also send tokens. -//! -//! Later, a network Wallet store can be implemented thusly: -//! 1. Chunk each CashNote, both spent and available. -//! 2. For a semi-public Wallet: -//! a. Store a register with address of your `MainPubkey`. -//! Then push these ops: -//! b. self.address.encrypt(Deposit(ChunkAddress)) -//! c. self.address.encrypt(Spend(ChunkAddress)) -//! And when the register has used 1023 entries: -//! d. self.address.encrypt(Extend(RegisterAddress)) -//! ... which would occupy the last entry, and thus link to a new register. -//! 3. For a private Wallet: -//! a. Store a register with address of self.address.encrypt(self.address). -//! ... then follow from b. in 2. -//! 4. Then, when a wallet is to be loaded from the network: -//! a. Get the `MainPubkey` from your secret. -//! b. Fetch the register with address of either the plaintext of or the encrypted `MainPubkey`. -//! c. Decrypt all entries and apply the ops to your Wallet, to get the current state of it. -//! d. If there is another register linked at the end of this one, follow that link and repeat steps b., c. and d. -//! -//! We will already now pave for that, by mimicing that flow for the local storage of a Wallet. -//! First though, a simpler local storage will be used. But after that a local register store can be implemented. -//! -//! ************************************************************************************************************ -//! -//! When the client spends a cash_note, ie signs the tx, the cash_note must be marked locally as spent (ie pending). -//! Only then should the client broadcast it. -//! -//! The client stores the tx as pending until either -//! a) all nodes respond with spent so the client locally changes it from pending to spent or -//! b) no nodes respond with spent so the client locally changes it to unspent. -//! -//! The best heuristic here is clients are in charge of their state, and the network is the source -//! of truth for the state. -//! If there’s ever a conflict in those states, the client can update their local state. -//! Clients create events (are in charge), nodes store events (are source of truth). -//! -//! The bitcoin flow here is very useful: unspent, unconfirmed (in mempool), confirmed. -//! These three states are held by both the client and the node, and is easy for the client to check and resolve. -//! -//! The most difficult situation for a bitcoin client to resolve is a low-fee tx in mempool for a long time, -//! which eventually clears from the mempool and becomes spendable again. -//! - -mod api; -mod authentication; -mod data_payments; -mod encryption; -mod error; -mod hot_wallet; -mod keys; -mod wallet_file; -mod watch_only; - -pub use self::{ - api::{WalletApi, WALLET_DIR_NAME}, - data_payments::{Payment, PaymentQuote, QuotingMetrics, QUOTE_EXPIRATION_SECS}, - error::{Error, Result}, - hot_wallet::HotWallet, - keys::bls_secret_from_hex, - wallet_file::wallet_lockfile_name, - watch_only::WatchOnlyWallet, -}; -pub(crate) use keys::store_new_keypair; - -use crate::{NanoTokens, UniquePubkey}; -use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, fs, path::Path}; -use wallet_file::wallet_file_name; - -#[derive(Default, Serialize, Deserialize)] -pub struct KeyLessWallet { - available_cash_notes: BTreeMap, -} - -impl KeyLessWallet { - /// Returns `Some(KeyLessWallet)` or None if file doesn't exist. - /// If the file is being written to, it will wait until the write is complete before reading. - pub fn load_from(wallet_dir: &Path) -> Result> { - let path = wallet_file_name(wallet_dir); - if !path.is_file() { - return Ok(None); - } - - let mut attempts = 0; - let mut wallet: Option = None; - - // Attempt to read the file and deserialize it. If the file is currently being written to, - // it will wait and try again. After 10 attempts, it will return an error. - while wallet.is_none() && attempts < 10 { - info!("Attempting to read wallet file"); - match fs::read(&path) { - Ok(data) => match rmp_serde::from_slice(&data) { - Ok(deserialized_wallet) => wallet = Some(deserialized_wallet), - Err(_) => { - attempts += 1; - info!("Attempt {attempts} to read wallet file failed... "); - std::thread::sleep(std::time::Duration::from_millis(100)); - } - }, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - attempts += 1; - info!("Attempt {attempts} to read wallet file failed... "); - std::thread::sleep(std::time::Duration::from_millis(100)); - } - Err(e) => return Err(Error::from(e)), - } - } - - // If the file could not be read and deserialized after 10 attempts, return an error. - if wallet.is_none() { - return Err(Error::from(std::io::Error::new( - std::io::ErrorKind::Other, - "Could not read and deserialize wallet file after multiple attempts", - ))); - } - - Ok(wallet) - } - - pub fn balance(&self) -> NanoTokens { - let mut balance = 0; - for (_unique_pubkey, value) in self.available_cash_notes.iter() { - balance += value.as_nano(); - } - NanoTokens::from(balance) - } -} diff --git a/sn_transfers/src/wallet/api.rs b/sn_transfers/src/wallet/api.rs deleted file mode 100644 index 6ae684d00f..0000000000 --- a/sn_transfers/src/wallet/api.rs +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::{data_payments::PaymentDetails, Result}; -use crate::WalletError; -use serde::Serialize; -use std::{ - fs, - path::{Path, PathBuf}, - sync::Arc, -}; -use xor_name::XorName; - -const PAYMENTS_DIR_NAME: &str = "payments"; -pub const WALLET_DIR_NAME: &str = "wallet"; - -/// Contains some common API's used by wallet implementations. -#[derive(serde::Serialize, serde::Deserialize, Clone)] -pub struct WalletApi { - /// The dir of the wallet file, main key, public address, and new cash_notes. - wallet_dir: Arc, - /// Cached version of `root_dir/wallet_dir/payments` - payment_dir: Arc, -} - -impl WalletApi { - /// Create a new instance give the root dir. - pub fn new_from_root_dir(root_dir: &Path) -> Self { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - Self { - payment_dir: Arc::new(wallet_dir.join(PAYMENTS_DIR_NAME)), - wallet_dir: Arc::new(wallet_dir), - } - } - - /// Create a new instance give the root dir. - pub fn new_from_wallet_dir(wallet_dir: &Path) -> Self { - Self { - wallet_dir: Arc::new(wallet_dir.to_path_buf()), - payment_dir: Arc::new(wallet_dir.join(PAYMENTS_DIR_NAME)), - } - } - - /// Returns the most recent PaymentDetails for the given xorname if cached. - /// If multiple payments have been made to the same xorname, then we pick the last one as it is the most recent. - pub fn get_recent_payment(&self, xorname: &XorName) -> Result { - let mut payments = self.read_payment_transactions(xorname)?; - let payment = payments - .pop() - .ok_or(WalletError::NoPaymentForAddress(*xorname))?; - info!("Payment retrieved for {xorname:?} from wallet"); - - Ok(payment) - } - - /// Return all the PaymentDetails for the given xorname if cached. - /// Multiple payments to the same XorName can result in many payment details - pub fn get_all_payments(&self, xorname: &XorName) -> Result> { - let payments = self.read_payment_transactions(xorname)?; - if payments.is_empty() { - return Err(WalletError::NoPaymentForAddress(*xorname)); - } - info!( - "All {} payments retrieved for {xorname:?} from wallet", - payments.len() - ); - - Ok(payments) - } - - /// Insert a payment and write it to the `payments` dir. - /// If a prior payment has been made to the same xorname, then the new payment is pushed to the end of the list. - pub fn insert_payment_transaction(&self, name: XorName, payment: PaymentDetails) -> Result<()> { - // try to read the previous payments and push the new payment at the end - let payments = match self.read_payment_transactions(&name) { - Ok(mut stored_payments) => { - stored_payments.push(payment); - stored_payments - } - Err(_) => vec![payment], - }; - let unique_file_name = format!("{}.payment", hex::encode(name)); - fs::create_dir_all(self.payment_dir.as_ref())?; - - let payment_file_path = self.payment_dir.join(unique_file_name); - debug!("Writing payment to {payment_file_path:?}"); - - let mut file = fs::File::create(payment_file_path)?; - let mut serialiser = rmp_serde::encode::Serializer::new(&mut file); - payments.serialize(&mut serialiser)?; - Ok(()) - } - - pub fn remove_payment_transaction(&self, name: &XorName) { - let unique_file_name = format!("{}.payment", hex::encode(*name)); - let payment_file_path = self.payment_dir.join(unique_file_name); - - debug!("Removing payment from {payment_file_path:?}"); - let _ = fs::remove_file(payment_file_path); - } - - pub fn wallet_dir(&self) -> &Path { - &self.wallet_dir - } - - /// Read all the payments made to the provided xorname - fn read_payment_transactions(&self, name: &XorName) -> Result> { - let unique_file_name = format!("{}.payment", hex::encode(*name)); - let payment_file_path = self.payment_dir.join(unique_file_name); - - debug!("Getting payment from {payment_file_path:?}"); - let file = fs::File::open(&payment_file_path)?; - let payments = rmp_serde::from_read(&file)?; - - Ok(payments) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use crate::{MainSecretKey, NanoTokens, PaymentQuote, Transfer}; - - #[test] - fn payment_selective() -> Result<()> { - let root_dir = std::env::temp_dir(); - let wallet_api = WalletApi::new_from_wallet_dir(&root_dir); - - let mut rng = bls::rand::thread_rng(); - let chunk_name = XorName::random(&mut rng); - - let transfer = Transfer::NetworkRoyalties(vec![]); - - let recipient_1 = MainSecretKey::random().main_pubkey(); - let payment_details_1 = PaymentDetails { - recipient: recipient_1, - peer_id_bytes: vec![], - transfer: (transfer.clone(), NanoTokens::zero()), - royalties: (transfer.clone(), NanoTokens::zero()), - quote: PaymentQuote::zero(), - }; - let _ = wallet_api.insert_payment_transaction(chunk_name, payment_details_1); - - let recipient_2 = MainSecretKey::random().main_pubkey(); - let payment_details_2 = PaymentDetails { - recipient: recipient_2, - peer_id_bytes: vec![], - transfer: (transfer.clone(), NanoTokens::zero()), - royalties: (transfer, NanoTokens::zero()), - quote: PaymentQuote::zero(), - }; - let _ = wallet_api.insert_payment_transaction(chunk_name, payment_details_2.clone()); - - let recent_payment = wallet_api.get_recent_payment(&chunk_name)?; - assert_eq!(payment_details_2.recipient, recent_payment.recipient); - - let recent_payment = wallet_api.get_recent_payment(&chunk_name)?; - assert_eq!(payment_details_2.recipient, recent_payment.recipient); - - Ok(()) - } -} diff --git a/sn_transfers/src/wallet/authentication.rs b/sn_transfers/src/wallet/authentication.rs deleted file mode 100644 index ed58273c30..0000000000 --- a/sn_transfers/src/wallet/authentication.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::wallet::encryption::EncryptedSecretKey; -use crate::wallet::{Error, Result}; -use chrono::{DateTime, Duration, Utc}; -use secrecy::{ExposeSecret, Secret}; -use std::path::PathBuf; - -/// Time (in seconds) before the user has to provide the password again for an encrypted wallet -const PASSWORD_EXPIRATION_TIME_SECS: i64 = 120; - -/// Manager that makes it easier to interact with encrypted wallets -pub struct AuthenticationManager { - /// Password to decrypt the wallet. - /// Wrapped in Secret<> so that it doesn't accidentally get exposed - password: Option>, - /// Expiry time of the password. - /// Has to be provided by the user again after a certain amount of time - password_expires_at: Option>, - /// Path to the root directory of the wallet - wallet_dir: PathBuf, -} - -impl AuthenticationManager { - pub fn new(wallet_dir: PathBuf) -> Self { - Self { - password: None, - password_expires_at: None, - wallet_dir, - } - } - - /// Authenticates the wallet using the provided password. - /// Password will be saved (available) for a limited amount of time. - pub fn authenticate_with_password(&mut self, password: String) -> Result<()> { - self.verify_password(&password)?; - self.password = Some(Secret::new(password)); - self.reset_password_expiration_time(); - Ok(()) - } - - /// Verifies the provided password against the encrypted secret key. - fn verify_password(&self, password: &str) -> Result<()> { - let encrypted_secret_key = EncryptedSecretKey::from_file(self.wallet_dir.as_path())?; - // Check if password is correct by trying to decrypt - encrypted_secret_key.decrypt(password)?; - Ok(()) - } - - /// Resets the password expiration time to the current time plus the expiration duration. - fn reset_password_expiration_time(&mut self) { - self.password_expires_at = - Some(Utc::now() + Duration::seconds(PASSWORD_EXPIRATION_TIME_SECS)); - } - - /// Authenticates the wallet and returns the password if it is encrypted. - /// - /// # Returns - /// - `Ok(Some(String))`: The wallet is encrypted and the password is available and valid. - /// - `Ok(None)`: The wallet is not encrypted. - /// - `Err(Error)`: The wallet is encrypted, but no valid password is available. - /// - /// # Errors - /// Returns an error in the following cases: - /// - `Error::WalletPasswordExpired`: The wallet's password has expired and the user needs to authenticate again with a valid password using `authenticate_with_password()`. - /// - `Error::WalletPasswordRequired`: The wallet is encrypted but no password is set. The user needs to authenticate with a valid password using `authenticate_with_password()`. - pub fn authenticate(&mut self) -> Result> { - // If wallet is encrypted, require a valid password - if EncryptedSecretKey::file_exists(self.wallet_dir.as_path()) { - // Check if a password is set - if let (Some(password), Some(expiration_time)) = - (&self.password.to_owned(), self.password_expires_at) - { - let password = password.expose_secret().to_owned(); - - // Verify if password is still correct - if self.verify_password(&password).is_err() { - self.password = None; - return Err(Error::WalletPasswordIncorrect); - } - - // Check if password hasn't expired - if Utc::now() <= expiration_time { - // Renew password expiration time after authenticating - self.reset_password_expiration_time(); - Ok(Some(password)) - } else { - // Password is no longer active. - // User needs to authenticate again with a valid password - self.password = None; - Err(Error::WalletPasswordExpired) - } - } else { - // User needs to authenticate with a valid password - Err(Error::WalletPasswordRequired) - } - } else { - // Wallet is not encrypted - Ok(None) - } - } -} diff --git a/sn_transfers/src/wallet/data_payments.rs b/sn_transfers/src/wallet/data_payments.rs deleted file mode 100644 index 7ff31f065a..0000000000 --- a/sn_transfers/src/wallet/data_payments.rs +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::{MainPubkey, NanoTokens, Transfer}; -use libp2p::{identity::PublicKey, PeerId}; -use serde::{Deserialize, Serialize}; -use std::time::SystemTime; -use xor_name::XorName; - -/// The time in seconds that a quote is valid for -pub const QUOTE_EXPIRATION_SECS: u64 = 3600; - -#[allow(dead_code)] -/// The margin allowed for live_time -const LIVE_TIME_MARGIN: u64 = 10; - -#[derive(Clone, Serialize, Deserialize, Eq, PartialEq, custom_debug::Debug)] -pub struct Payment { - /// The transfers we make - #[debug(skip)] - pub transfers: Vec, - /// The Quote we're paying for - pub quote: PaymentQuote, -} - -/// Information relating to a data payment for one address -#[derive(Clone, Serialize, Deserialize)] -pub struct PaymentDetails { - /// The node we pay - pub recipient: MainPubkey, - /// The PeerId (as bytes) of the node we pay. - /// The PeerId is not stored here to avoid direct dependency with libp2p, - /// plus it doesn't implement Serialize/Deserialize traits. - pub peer_id_bytes: Vec, - /// The transfer we send to it and its amount as reference - pub transfer: (Transfer, NanoTokens), - /// The network Royalties - pub royalties: (Transfer, NanoTokens), - /// The original quote - pub quote: PaymentQuote, -} - -impl PaymentDetails { - /// create a Payment for a PaymentDetails - pub fn to_payment(&self) -> Payment { - Payment { - transfers: vec![self.transfer.0.clone(), self.royalties.0.clone()], - quote: self.quote.clone(), - } - } -} - -/// A generic type for signatures -pub type QuoteSignature = Vec; - -/// Quoting metrics that got used to generate a quote, or to track peer's status. -#[derive( - Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, custom_debug::Debug, -)] -pub struct QuotingMetrics { - /// the records stored - pub close_records_stored: usize, - /// the max_records configured - pub max_records: usize, - /// number of times that got paid - pub received_payment_count: usize, - /// the duration that node keeps connected to the network, measured in hours - /// TODO: take `restart` into accout - pub live_time: u64, -} - -impl QuotingMetrics { - /// construct an empty QuotingMetrics - pub fn new() -> Self { - Self { - close_records_stored: 0, - max_records: 0, - received_payment_count: 0, - live_time: 0, - } - } -} - -impl Default for QuotingMetrics { - fn default() -> Self { - Self::new() - } -} - -/// A payment quote to store data given by a node to a client -/// Note that the PaymentQuote is a contract between the node and itself to make sure the clients aren’t mispaying. -/// It is NOT a contract between the client and the node. -#[derive( - Clone, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, custom_debug::Debug, -)] -pub struct PaymentQuote { - /// the content paid for - pub content: XorName, - /// how much the node demands for storing the content - pub cost: NanoTokens, - /// the local node time when the quote was created - pub timestamp: SystemTime, - /// quoting metrics being used to generate this quote - pub quoting_metrics: QuotingMetrics, - /// list of bad_nodes that client shall not pick as a payee - /// in `serialised` format to avoid cyclic dependent on sn_protocol - #[debug(skip)] - pub bad_nodes: Vec, - /// node's public key that can verify the signature - #[debug(skip)] - pub pub_key: Vec, - #[debug(skip)] - pub signature: QuoteSignature, -} - -impl PaymentQuote { - /// create an empty PaymentQuote - pub fn zero() -> Self { - Self { - content: Default::default(), - cost: NanoTokens::zero(), - timestamp: SystemTime::now(), - quoting_metrics: Default::default(), - bad_nodes: vec![], - pub_key: vec![], - signature: vec![], - } - } - - /// returns the bytes to be signed - pub fn bytes_for_signing( - xorname: XorName, - cost: NanoTokens, - timestamp: SystemTime, - quoting_metrics: &QuotingMetrics, - serialised_bad_nodes: &[u8], - ) -> Vec { - let mut bytes = xorname.to_vec(); - bytes.extend_from_slice(&cost.to_bytes()); - bytes.extend_from_slice( - ×tamp - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Unix epoch to be in the past") - .as_secs() - .to_le_bytes(), - ); - let serialised_quoting_metrics = rmp_serde::to_vec(quoting_metrics).unwrap_or_default(); - bytes.extend_from_slice(&serialised_quoting_metrics); - bytes.extend_from_slice(serialised_bad_nodes); - bytes - } - - /// Check self is signed by the claimed peer - pub fn check_is_signed_by_claimed_peer(&self, claimed_peer: PeerId) -> bool { - let pub_key = if let Ok(pub_key) = PublicKey::try_decode_protobuf(&self.pub_key) { - pub_key - } else { - error!("Cann't parse PublicKey from protobuf"); - return false; - }; - - let self_peer_id = PeerId::from(pub_key.clone()); - - if self_peer_id != claimed_peer { - error!("This quote {self:?} of {self_peer_id:?} is not signed by {claimed_peer:?}"); - return false; - } - - let bytes = Self::bytes_for_signing( - self.content, - self.cost, - self.timestamp, - &self.quoting_metrics, - &self.bad_nodes, - ); - - if !pub_key.verify(&bytes, &self.signature) { - error!("Signature is not signed by claimed pub_key"); - return false; - } - - true - } - - /// Returns true if the quote has not yet expired - pub fn has_expired(&self) -> bool { - let now = std::time::SystemTime::now(); - - let dur_s = match now.duration_since(self.timestamp) { - Ok(dur) => dur.as_secs(), - Err(err) => { - info!( - "Cann't deduce elapsed time from {:?} with error {err:?}", - self.timestamp - ); - return true; - } - }; - dur_s > QUOTE_EXPIRATION_SECS - } - - /// test utility to create a dummy quote - pub fn test_dummy(xorname: XorName, cost: NanoTokens) -> Self { - Self { - content: xorname, - cost, - timestamp: SystemTime::now(), - quoting_metrics: Default::default(), - bad_nodes: vec![], - pub_key: vec![], - signature: vec![], - } - } - - /// Check whether self is newer than the target quote. - pub fn is_newer_than(&self, other: &Self) -> bool { - self.timestamp > other.timestamp - } - - /// Check against a new quote, verify whether it is a valid one from self perspective. - /// Returns `true` to flag the `other` quote is valid, from self perspective. - pub fn historical_verify(&self, _other: &Self) -> bool { - // TODO: Shall be refactored once new quote filtering scheme deployed - true - // // There is a chance that an old quote got used later than a new quote - // let self_is_newer = self.is_newer_than(other); - // let (old_quote, new_quote) = if self_is_newer { - // (other, self) - // } else { - // (self, other) - // }; - - // if new_quote.quoting_metrics.live_time < old_quote.quoting_metrics.live_time { - // info!("Claimed live_time out of sequence"); - // return false; - // } - - // let old_elapsed = if let Ok(elapsed) = old_quote.timestamp.elapsed() { - // elapsed - // } else { - // info!("timestamp failure"); - // return false; - // }; - // let new_elapsed = if let Ok(elapsed) = new_quote.timestamp.elapsed() { - // elapsed - // } else { - // info!("timestamp failure"); - // return false; - // }; - - // let time_diff = old_elapsed.as_secs().saturating_sub(new_elapsed.as_secs()); - // let live_time_diff = - // new_quote.quoting_metrics.live_time - old_quote.quoting_metrics.live_time; - // // In theory, these two shall match, give it a LIVE_TIME_MARGIN to avoid system glitch - // if live_time_diff > time_diff + LIVE_TIME_MARGIN { - // info!("claimed live_time out of sync with the timestamp"); - // return false; - // } - - // // There could be pruning to be undertaken, also the close range keeps changing as well. - // // Hence `close_records_stored` could be growing or shrinking. - // // Currently not to carry out check on it, just logging to observe the trend. - // debug!( - // "The new quote has {} close records stored, meanwhile old one has {}.", - // new_quote.quoting_metrics.close_records_stored, - // old_quote.quoting_metrics.close_records_stored - // ); - - // // TODO: Double check if this applies, as this will prevent a node restart with same ID - // if new_quote.quoting_metrics.received_payment_count - // < old_quote.quoting_metrics.received_payment_count - // { - // info!("claimed received_payment_count out of sequence"); - // return false; - // } - - // true - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use libp2p::identity::Keypair; - use std::{thread::sleep, time::Duration}; - - #[test] - fn test_is_newer_than() { - let old_quote = PaymentQuote::zero(); - sleep(Duration::from_millis(100)); - let new_quote = PaymentQuote::zero(); - assert!(new_quote.is_newer_than(&old_quote)); - assert!(!old_quote.is_newer_than(&new_quote)); - } - - #[test] - fn test_is_signed_by_claimed_peer() { - let keypair = Keypair::generate_ed25519(); - let peer_id = keypair.public().to_peer_id(); - - let false_peer = PeerId::random(); - - let mut quote = PaymentQuote::zero(); - let bytes = PaymentQuote::bytes_for_signing( - quote.content, - quote.cost, - quote.timestamp, - "e.quoting_metrics, - &[], - ); - let signature = if let Ok(sig) = keypair.sign(&bytes) { - sig - } else { - panic!("Cannot sign the quote!"); - }; - - // Check failed with both incorrect pub_key and signature - assert!(!quote.check_is_signed_by_claimed_peer(peer_id)); - assert!(!quote.check_is_signed_by_claimed_peer(false_peer)); - - // Check failed with correct pub_key but incorrect signature - quote.pub_key = keypair.public().encode_protobuf(); - assert!(!quote.check_is_signed_by_claimed_peer(peer_id)); - assert!(!quote.check_is_signed_by_claimed_peer(false_peer)); - - // Check succeed with correct pub_key and signature, - // and failed with incorrect claimed signer (peer) - quote.signature = signature; - assert!(quote.check_is_signed_by_claimed_peer(peer_id)); - assert!(!quote.check_is_signed_by_claimed_peer(false_peer)); - - // Check failed with incorrect pub_key but correct signature - quote.pub_key = Keypair::generate_ed25519().public().encode_protobuf(); - assert!(!quote.check_is_signed_by_claimed_peer(peer_id)); - assert!(!quote.check_is_signed_by_claimed_peer(false_peer)); - } - - #[ignore = "Shall be refactored once new quote filtering scheme deployed"] - #[test] - fn test_historical_verify() { - let mut old_quote = PaymentQuote::zero(); - sleep(Duration::from_millis(100)); - let mut new_quote = PaymentQuote::zero(); - - // historical_verify will swap quotes to compare based on timeline automatically - assert!(new_quote.historical_verify(&old_quote)); - assert!(old_quote.historical_verify(&new_quote)); - - // Out of sequence received_payment_count shall be detected - old_quote.quoting_metrics.received_payment_count = 10; - new_quote.quoting_metrics.received_payment_count = 9; - assert!(!new_quote.historical_verify(&old_quote)); - assert!(!old_quote.historical_verify(&new_quote)); - // Reset to correct one - new_quote.quoting_metrics.received_payment_count = 11; - assert!(new_quote.historical_verify(&old_quote)); - assert!(old_quote.historical_verify(&new_quote)); - - // Out of sequence live_time shall be detected - new_quote.quoting_metrics.live_time = 10; - old_quote.quoting_metrics.live_time = 11; - assert!(!new_quote.historical_verify(&old_quote)); - assert!(!old_quote.historical_verify(&new_quote)); - // Out of margin live_time shall be detected - new_quote.quoting_metrics.live_time = 11 + LIVE_TIME_MARGIN + 1; - assert!(!new_quote.historical_verify(&old_quote)); - assert!(!old_quote.historical_verify(&new_quote)); - // Reset live_time to be within the margin - new_quote.quoting_metrics.live_time = 11 + LIVE_TIME_MARGIN - 1; - assert!(new_quote.historical_verify(&old_quote)); - assert!(old_quote.historical_verify(&new_quote)); - } -} diff --git a/sn_transfers/src/wallet/encryption.rs b/sn_transfers/src/wallet/encryption.rs deleted file mode 100644 index c0ae28aaa1..0000000000 --- a/sn_transfers/src/wallet/encryption.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::wallet::Error; -use crate::wallet::Result; -use crate::MainSecretKey; -use bls::SecretKey; -use hex::encode; -use rand::Rng; -use ring::aead::{BoundKey, Nonce, NonceSequence}; -use ring::error::Unspecified; -use serde::{Deserialize, Serialize}; -use std::io::Read; -use std::num::NonZeroU32; -use std::path::Path; - -/// Number of iterations for pbkdf2. -const ITERATIONS: NonZeroU32 = match NonZeroU32::new(100_000) { - Some(v) => v, - None => panic!("`100_000` is not be zero"), -}; - -/// Filename for the encrypted secret key. -pub const ENCRYPTED_MAIN_SECRET_KEY_FILENAME: &str = "main_secret_key.encrypted"; - -/// Encrypted secret key for storing on disk and decrypting with password -#[derive(Serialize, Deserialize)] -pub(crate) struct EncryptedSecretKey { - encrypted_secret_key: String, - pub salt: String, - pub nonce: String, -} - -impl EncryptedSecretKey { - /// Save an encrypted secret key to a file inside the wallet directory. - /// The encrypted secret key will be saved as `main_secret_key.encrypted`. - pub fn save_to_file(&self, wallet_dir: &Path) -> Result<()> { - let serialized_data = serde_json::to_string(&self) - .map_err(|e| Error::FailedToSerializeEncryptedKey(e.to_string()))?; - - let encrypted_secret_key_path = wallet_dir.join(ENCRYPTED_MAIN_SECRET_KEY_FILENAME); - - std::fs::write(encrypted_secret_key_path, serialized_data)?; - - Ok(()) - } - - /// Read an encrypted secret key from file. - /// The file should be named `main_secret_key.encrypted` and inside the provided wallet directory. - pub fn from_file(wallet_dir: &Path) -> Result { - let path = wallet_dir.join(ENCRYPTED_MAIN_SECRET_KEY_FILENAME); - - if !path.is_file() { - return Err(Error::EncryptedMainSecretKeyNotFound(path)); - } - - let mut file = std::fs::File::open(path).map_err(|_| { - Error::FailedToDeserializeEncryptedKey(String::from("File open failed.")) - })?; - - let mut buffer = String::new(); - - file.read_to_string(&mut buffer).map_err(|_| { - Error::FailedToDeserializeEncryptedKey(String::from("File read failed.")) - })?; - - let encrypted_secret_key: EncryptedSecretKey = - serde_json::from_str(&buffer).map_err(|_| { - Error::FailedToDeserializeEncryptedKey(format!("Deserialization failed: {buffer}")) - })?; - - Ok(encrypted_secret_key) - } - - /// Returns whether a `main_secret_key.encrypted` file exists. - pub fn file_exists(wallet_dir: &Path) -> bool { - let path = wallet_dir.join(ENCRYPTED_MAIN_SECRET_KEY_FILENAME); - path.is_file() - } - - /// Decrypt an encrypted secret key using the password. - pub fn decrypt(&self, password: &str) -> Result { - let salt = hex::decode(&self.salt) - .map_err(|_| Error::FailedToDecryptKey(String::from("Invalid salt encoding.")))?; - - let mut key = [0; 32]; - - // Reconstruct the key from salt and password - ring::pbkdf2::derive( - ring::pbkdf2::PBKDF2_HMAC_SHA512, - ITERATIONS, - &salt, - password.as_bytes(), - &mut key, - ); - - // Create an unbound key from the previously reconstructed key - let unbound_key = ring::aead::UnboundKey::new(&ring::aead::CHACHA20_POLY1305, &key) - .map_err(|_| { - Error::FailedToDecryptKey(String::from("Could not create unbound key.")) - })?; - - // Restore original nonce - let nonce_vec = hex::decode(&self.nonce) - .map_err(|_| Error::FailedToDecryptKey(String::from("Invalid nonce encoding.")))?; - - let mut nonce = [0u8; 12]; - nonce.copy_from_slice(&nonce_vec[0..12]); - - // Create an opening key using the unbound key and original nonce - let mut opening_key = ring::aead::OpeningKey::new(unbound_key, NonceSeq(nonce)); - let aad = ring::aead::Aad::from(&[]); - - // Convert the hex encoded and encrypted secret key to bytes - let mut encrypted_secret_key = hex::decode(&self.encrypted_secret_key).map_err(|_| { - Error::FailedToDecryptKey(String::from("Invalid encrypted secret key encoding.")) - })?; - - // Decrypt the encrypted secret key bytes - let decrypted_data = opening_key - .open_in_place(aad, &mut encrypted_secret_key) - .map_err(|_| Error::FailedToDecryptKey(String::from("Could not open encrypted key")))?; - - let mut secret_key_bytes = [0u8; 32]; - secret_key_bytes.copy_from_slice(&decrypted_data[0..32]); - - // Create secret key from decrypted bytes - let secret_key = SecretKey::from_bytes(secret_key_bytes)?; - - Ok(MainSecretKey::new(secret_key)) - } -} - -/// Nonce sequence for the aead sealing key. -struct NonceSeq([u8; 12]); - -impl NonceSequence for NonceSeq { - fn advance(&mut self) -> std::result::Result { - Nonce::try_assume_unique_for_key(&self.0) - } -} - -/// Encrypts secret key using pbkdf2 with HMAC. -pub(crate) fn encrypt_secret_key( - secret_key: &MainSecretKey, - password: &str, -) -> Result { - // Generate a random salt - // Salt is used to ensure unique derived keys even for identical passwords - let mut salt = [0u8; 8]; - rand::thread_rng().fill(&mut salt); - - // Generate a random nonce - // Nonce is used to ensure unique encryption outputs even for identical inputs - let mut nonce = [0u8; 12]; - rand::thread_rng().fill(&mut nonce); - - let mut key = [0; 32]; - - // Derive a key from the password using PBKDF2 with HMAC - // PBKDF2 is used for key derivation to mitigate brute-force attacks by making key derivation computationally expensive - // HMAC is used as the pseudorandom function for its security properties - ring::pbkdf2::derive( - ring::pbkdf2::PBKDF2_HMAC_SHA512, - ITERATIONS, - &salt, - password.as_bytes(), - &mut key, - ); - - // Create an unbound key using CHACHA20_POLY1305 algorithm - // CHACHA20_POLY1305 is a fast and secure AEAD (Authenticated Encryption with Associated Data) algorithm - let unbound_key = ring::aead::UnboundKey::new(&ring::aead::CHACHA20_POLY1305, &key) - .map_err(|_| Error::FailedToEncryptKey(String::from("Could not create unbound key.")))?; - - // Create a sealing key with the unbound key and nonce - let mut sealing_key = ring::aead::SealingKey::new(unbound_key, NonceSeq(nonce)); - let aad = ring::aead::Aad::from(&[]); - - // Convert the secret key to bytes - let secret_key_bytes = secret_key.to_bytes(); - let mut encrypted_secret_key = secret_key_bytes; - - // seal_in_place_append_tag encrypts the data and appends an authentication tag to ensure data integrity - sealing_key - .seal_in_place_append_tag(aad, &mut encrypted_secret_key) - .map_err(|_| Error::FailedToEncryptKey(String::from("Could not seal sealing key.")))?; - - // Return the encrypted secret key along with salt and nonce encoded as hex strings - Ok(EncryptedSecretKey { - encrypted_secret_key: encode(encrypted_secret_key), - salt: encode(salt), - nonce: encode(nonce), - }) -} - -#[cfg(test)] -mod tests { - use crate::wallet::encryption::{ - encrypt_secret_key, EncryptedSecretKey, ENCRYPTED_MAIN_SECRET_KEY_FILENAME, - }; - use crate::MainSecretKey; - use bls::SecretKey; - - /// Helper function to create a random MainSecretKey for testing. - fn generate_main_secret_key() -> MainSecretKey { - let secret_key = SecretKey::random(); - MainSecretKey::new(secret_key) - } - - #[test] - fn test_encrypt_and_decrypt() { - let password = "safenetwork"; - let main_secret_key = generate_main_secret_key(); - - // Encrypt the secret key - let encrypted_secret_key = - encrypt_secret_key(&main_secret_key, password).expect("Failed to encrypt key"); - - // Decrypt the secret key - let decrypted_secret_key = encrypted_secret_key - .decrypt(password) - .expect("Failed to decrypt key"); - - // Ensure the decrypted key matches the original key - assert_eq!(main_secret_key.to_bytes(), decrypted_secret_key.to_bytes()); - } - - #[test] - fn test_decrypt_with_wrong_password() { - let password = "safenetwork"; - let wrong_password = "unsafenetwork"; - let main_secret_key = generate_main_secret_key(); - - // Encrypt the secret key - let encrypted_secret_key = - encrypt_secret_key(&main_secret_key, password).expect("Failed to encrypt key"); - - // Ensure the decryption succeeds with the correct password - assert!(encrypted_secret_key.decrypt(password).is_ok()); - - // Ensure the decryption fails with the wrong password - assert!(encrypted_secret_key.decrypt(wrong_password).is_err()); - } - - #[test] - fn test_save_to_file_and_read_from_file() { - let password = "safenetwork"; - let main_secret_key = generate_main_secret_key(); - let encrypted_secret_key = - encrypt_secret_key(&main_secret_key, password).expect("Failed to encrypt key"); - - // Create a temporary directory - let temp_dir = tempfile::tempdir().unwrap(); - let wallet_dir = temp_dir.path(); - - // Save the encrypted secret key to the file - encrypted_secret_key - .save_to_file(wallet_dir) - .expect("Failed to save encrypted key to file"); - - // Check if the file exists - let encrypted_secret_key_path = wallet_dir.join(ENCRYPTED_MAIN_SECRET_KEY_FILENAME); - assert!( - encrypted_secret_key_path.is_file(), - "Encrypted key file does not exist" - ); - - // Read the file - let read_encrypted_secret_key = EncryptedSecretKey::from_file(wallet_dir) - .expect("Failed to read encrypted key from file."); - - // Ensure the read data matches the original encrypted secret key - assert_eq!( - read_encrypted_secret_key.encrypted_secret_key, - encrypted_secret_key.encrypted_secret_key - ); - assert_eq!(read_encrypted_secret_key.salt, encrypted_secret_key.salt); - assert_eq!(read_encrypted_secret_key.nonce, encrypted_secret_key.nonce); - } - - #[test] - fn test_file_exists() { - // todo - } -} diff --git a/sn_transfers/src/wallet/error.rs b/sn_transfers/src/wallet/error.rs deleted file mode 100644 index 5a57b7434a..0000000000 --- a/sn_transfers/src/wallet/error.rs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use crate::UniquePubkey; -use std::{collections::BTreeSet, path::PathBuf}; -use thiserror::Error; -use xor_name::XorName; - -/// Specialisation of `std::Result`. -pub type Result = std::result::Result; - -/// Transfer errors. -#[derive(Debug, Error)] -pub enum Error { - /// The cashnotes that were attempted to be spent have already been spent to another address - #[error("Attempted to reload a wallet from disk, but the disk wallet is not the same as the current wallet. Wallet path: {0}")] - CurrentAndLoadedKeyMismatch(PathBuf), - - /// The cashnotes that were attempted to be spent have already been spent to another address - #[error("Double spend attempted with cashnotes: {0:?}")] - DoubleSpendAttemptedForCashNotes(BTreeSet), - - /// Address provided is of the wrong type - #[error("Invalid address type")] - InvalidAddressType, - /// CashNote add would overflow - #[error("Total price exceed possible token amount")] - TotalPriceTooHigh, - /// A general error when a transfer fails - #[error("Failed to send tokens due to {0}")] - CouldNotSendMoney(String), - /// Failed to sign a transaction - #[error("Failed to sign a transaction: {0}")] - CouldNotSignTransaction(String), - /// A general error when receiving a transfer fails - #[error("Failed to receive transfer due to {0}")] - CouldNotReceiveMoney(String), - /// A general error when verifying a transfer validity in the network - #[error("Failed to verify transfer validity in the network {0}")] - CouldNotVerifyTransfer(String), - /// Failed to fetch spend from network - #[error("Failed to fetch spend from network: {0}")] - FailedToGetSpend(String), - /// Failed to send spend for processing - #[error("Failed to send spend for processing: {0}")] - SpendProcessing(String), - /// Failed to parse bytes into a bls key - #[error("Unconfirmed transactions still persist even after retries")] - UnconfirmedTxAfterRetries, - /// Main pub key doesn't match the key found when loading wallet from path - #[error("Main pub key doesn't match the key found when loading wallet from path: {0:#?}")] - PubKeyMismatch(std::path::PathBuf), - /// Main pub key not found when loading wallet from path - #[error("Main pub key not found: {0:#?}")] - PubkeyNotFound(std::path::PathBuf), - /// Main secret key not found when loading wallet from path - #[error("Main secret key not found: {0:#?}")] - MainSecretKeyNotFound(std::path::PathBuf), - /// Encrypted main secret key not found when loading wallet from path - #[error("Encrypted main secret key not found: {0:#?}")] - EncryptedMainSecretKeyNotFound(std::path::PathBuf), - /// Encrypted main secret key requires a password to decrypt - #[error("Encrypted main secret key requires a password")] - EncryptedMainSecretKeyRequiresPassword, - /// Failed to serialize encrypted secret key - #[error("Failed to serialize encrypted secret key: {0}")] - FailedToSerializeEncryptedKey(String), - /// Failed to deserialize encrypted secret key - #[error("Failed to deserialize encrypted secret key: {0}")] - FailedToDeserializeEncryptedKey(String), - /// Failed to encrypt a secret key - #[error("Failed to encrypt secret key: {0}")] - FailedToEncryptKey(String), - /// Failed to decrypt a secret key - #[error("Failed to decrypt secret key: {0}")] - FailedToDecryptKey(String), - /// Failed to parse bytes into a bls key - #[error("Failed to parse bls key")] - FailedToParseBlsKey, - /// Failed to decode a hex string to a key - #[error("Could not decode hex string to key")] - FailedToDecodeHexToKey, - /// Failed to serialize a main key to hex - #[error("Could not serialize main key to hex: {0}")] - FailedToHexEncodeKey(String), - /// Failed to serialize a cashnote to a hex - #[error("Could not encode cashnote to hex")] - FailedToHexEncodeCashNote, - /// Failed to decypher transfer with our key, maybe it was encrypted to another key - #[error("Failed to decypher transfer with our key, maybe it was not for us")] - FailedToDecypherTransfer, - /// No cached payment found for address - #[error("No ongoing payment found for address {0:?}")] - NoPaymentForAddress(XorName), - /// The payment Quote has expired. - #[error("The payment quote made for {0:?} has expired")] - QuoteExpired(XorName), - - /// DAG error - #[error("DAG error: {0}")] - Dag(String), - /// Transfer error - #[error("Transfer error: {0}")] - Transfer(#[from] crate::TransferError), - /// Bls error - #[error("Bls error: {0}")] - Bls(#[from] bls::error::Error), - /// MsgPack serialisation error - #[error("MsgPack serialisation error:: {0}")] - Serialisation(#[from] rmp_serde::encode::Error), - /// MsgPack deserialisation error - #[error("MsgPack deserialisation error:: {0}")] - Deserialisation(#[from] rmp_serde::decode::Error), - /// I/O error - #[error("I/O error: {0}")] - Io(#[from] std::io::Error), - - /// Wallet password is incorrect - #[error("Wallet password is incorrect")] - WalletPasswordIncorrect, - /// Wallet is password protected - #[error("Wallet password required")] - WalletPasswordRequired, - /// Wallet password is only valid for a certain time until the user has to provide it again - #[error("Wallet password expired")] - WalletPasswordExpired, - /// Wallet is already encrypted - #[error("Wallet is already encrypted")] - WalletAlreadyEncrypted, -} diff --git a/sn_transfers/src/wallet/hot_wallet.rs b/sn_transfers/src/wallet/hot_wallet.rs deleted file mode 100644 index bf9872b652..0000000000 --- a/sn_transfers/src/wallet/hot_wallet.rs +++ /dev/null @@ -1,1280 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::{ - api::{WalletApi, WALLET_DIR_NAME}, - data_payments::{PaymentDetails, PaymentQuote}, - keys::{get_main_key_from_disk, store_new_keypair}, - wallet_file::{ - get_confirmed_spend, get_unconfirmed_spend_requests, has_confirmed_spend, - load_created_cash_note, remove_cash_notes, remove_unconfirmed_spend_requests, - store_created_cash_notes, store_unconfirmed_spend_requests, - }, - watch_only::WatchOnlyWallet, - Error, KeyLessWallet, Result, -}; -use crate::wallet::authentication::AuthenticationManager; -use crate::wallet::encryption::EncryptedSecretKey; -use crate::wallet::keys::{ - delete_encrypted_main_secret_key, delete_unencrypted_main_secret_key, get_main_pubkey, - store_main_secret_key, -}; -use crate::{ - calculate_royalties_fee, transfers::SignedTransaction, CashNote, CashNoteRedemption, - DerivationIndex, DerivedSecretKey, MainPubkey, MainSecretKey, NanoTokens, SignedSpend, - SpendAddress, SpendReason, Transfer, UniquePubkey, UnsignedTransaction, WalletError, - NETWORK_ROYALTIES_PK, -}; -use std::{ - collections::{BTreeMap, BTreeSet, HashSet}, - fs::File, - path::{Path, PathBuf}, - time::Instant, -}; -use xor_name::XorName; - -/// A locked file handle, that when dropped releases the lock. -pub type WalletExclusiveAccess = File; - -/// A hot-wallet. -pub struct HotWallet { - /// The secret key with which we can access - /// all the tokens in the available_cash_notes. - key: MainSecretKey, - /// The wallet containing all data. - watchonly_wallet: WatchOnlyWallet, - /// These have not yet been successfully sent to the network - /// and need to be, to reach network validity. - unconfirmed_spend_requests: BTreeSet, - /// Handles authentication of (encrypted) wallets. - authentication_manager: AuthenticationManager, -} - -impl HotWallet { - pub fn new(key: MainSecretKey, wallet_dir: PathBuf) -> Self { - let watchonly_wallet = - WatchOnlyWallet::new(key.main_pubkey(), &wallet_dir, KeyLessWallet::default()); - - Self { - key, - watchonly_wallet, - unconfirmed_spend_requests: Default::default(), - authentication_manager: AuthenticationManager::new(wallet_dir), - } - } - - pub fn key(&self) -> &MainSecretKey { - &self.key - } - - pub fn api(&self) -> &WalletApi { - self.watchonly_wallet.api() - } - - pub fn root_dir(&self) -> &Path { - self.watchonly_wallet.api().wallet_dir() - } - - pub fn wo_wallet(&self) -> &WatchOnlyWallet { - &self.watchonly_wallet - } - - pub fn wo_wallet_mut(&mut self) -> &mut WatchOnlyWallet { - &mut self.watchonly_wallet - } - - /// Returns whether a wallet in the specified directory is encrypted or not. - pub fn is_encrypted(root_dir: &Path) -> bool { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - EncryptedSecretKey::file_exists(&wallet_dir) - } - - /// Stores the wallet to disk. - /// This requires having exclusive access to the wallet to prevent concurrent processes from writing to it - fn store(&self, exclusive_access: WalletExclusiveAccess) -> Result<()> { - self.watchonly_wallet.store(exclusive_access) - } - - /// Reloads the wallet from disk. If the wallet secret key is encrypted, you'll need to specify the password. - fn reload(&mut self) -> Result<()> { - // Password needed to decrypt wallet if it is encrypted - let opt_password = self.authenticate()?; - - let wallet = - Self::load_from_path_and_key(self.watchonly_wallet.wallet_dir(), None, opt_password)?; - - if *wallet.key.secret_key() != *self.key.secret_key() { - return Err(WalletError::CurrentAndLoadedKeyMismatch( - self.watchonly_wallet.wallet_dir().into(), - )); - } - - // if it's a matching key, we can overwrite our wallet - *self = wallet; - Ok(()) - } - - /// Authenticates the wallet and returns the password if it is encrypted. - /// - /// # Returns - /// - `Ok(Some(String))`: The wallet is encrypted and the password is available. - /// - `Ok(None)`: The wallet is not encrypted. - /// - `Err`: The wallet is encrypted, but no password is available. - /// - /// # Errors - /// Returns an error if the wallet is encrypted and the password is not available. - /// In such cases, the password needs to be set using `authenticate_with_password()`. - pub fn authenticate(&mut self) -> Result> { - self.authentication_manager.authenticate() - } - - /// Authenticates the wallet and saves the password for a certain amount of time. - pub fn authenticate_with_password(&mut self, password: String) -> Result<()> { - self.authentication_manager - .authenticate_with_password(password) - } - - /// Encrypts wallet with a password. - /// - /// Fails if wallet is already encrypted. - pub fn encrypt(root_dir: &Path, password: &str) -> Result<()> { - if Self::is_encrypted(root_dir) { - return Err(Error::WalletAlreadyEncrypted); - } - - let wallet_key = Self::load_from(root_dir)?.key; - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - - // Save the secret key as an encrypted file - store_main_secret_key(&wallet_dir, &wallet_key, Some(password.to_owned()))?; - - // Delete the unencrypted secret key file - // Cleanup if it fails - if let Err(err) = delete_unencrypted_main_secret_key(&wallet_dir) { - let _ = delete_encrypted_main_secret_key(&wallet_dir); - return Err(err); - } - - Ok(()) - } - - /// Locks the wallet and returns exclusive access to the wallet - /// This lock prevents any other process from locking the wallet dir, effectively acts as a mutex for the wallet - pub fn lock(&self) -> Result { - self.watchonly_wallet.lock() - } - - /// Stores the given cash_notes to the `created cash_notes dir` in the wallet dir. - /// These can then be sent to the recipients out of band, over any channel preferred. - pub fn store_cash_notes_to_disk<'a, T>(&self, cash_notes: T) -> Result<()> - where - T: IntoIterator, - { - store_created_cash_notes(cash_notes, self.watchonly_wallet.wallet_dir()) - } - /// Removes the given cash_notes from the `created cash_notes dir` in the wallet dir. - pub fn remove_cash_notes_from_disk<'a, T>(&self, cash_notes: T) -> Result<()> - where - T: IntoIterator, - { - remove_cash_notes(cash_notes, self.watchonly_wallet.wallet_dir()) - } - - /// Store unconfirmed_spend_requests to disk. - pub fn store_unconfirmed_spend_requests(&mut self) -> Result<()> { - store_unconfirmed_spend_requests( - self.watchonly_wallet.wallet_dir(), - self.unconfirmed_spend_requests(), - ) - } - - /// Get confirmed spend from disk. - pub fn get_confirmed_spend(&mut self, spend_addr: SpendAddress) -> Result> { - get_confirmed_spend(self.watchonly_wallet.wallet_dir(), spend_addr) - } - - /// Check whether have the specific confirmed spend. - pub fn has_confirmed_spend(&mut self, spend_addr: SpendAddress) -> bool { - has_confirmed_spend(self.watchonly_wallet.wallet_dir(), spend_addr) - } - - /// Remove unconfirmed_spend_requests from disk. - fn remove_unconfirmed_spend_requests(&mut self) -> Result<()> { - remove_unconfirmed_spend_requests( - self.watchonly_wallet.wallet_dir(), - self.unconfirmed_spend_requests(), - ) - } - - /// Remove referenced CashNotes from available_cash_notes - pub fn mark_notes_as_spent<'a, T>(&mut self, unique_pubkeys: T) - where - T: IntoIterator, - { - self.watchonly_wallet.mark_notes_as_spent(unique_pubkeys); - } - - pub fn unconfirmed_spend_requests_exist(&self) -> bool { - !self.unconfirmed_spend_requests.is_empty() - } - - /// Try to load any new cash_notes from the `cash_notes dir` in the wallet dir. - pub fn try_load_cash_notes(&mut self) -> Result<()> { - self.watchonly_wallet.try_load_cash_notes() - } - - /// Loads a serialized wallet from a path and given main key. - pub fn load_from_main_key(root_dir: &Path, main_key: MainSecretKey) -> Result { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - // This creates the received_cash_notes dir if it doesn't exist. - std::fs::create_dir_all(&wallet_dir)?; - // This creates the main_key file if it doesn't exist. - Self::load_from_path_and_key(&wallet_dir, Some(main_key), None) - } - - /// Creates a serialized wallet for a path and main key. - /// This will overwrite any existing wallet, unlike load_from_main_key - pub fn create_from_key( - root_dir: &Path, - key: MainSecretKey, - password: Option, - ) -> Result { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - // This creates the received_cash_notes dir if it doesn't exist. - std::fs::create_dir_all(&wallet_dir)?; - // Create the new wallet for this key - store_new_keypair(&wallet_dir, &key, password)?; - let unconfirmed_spend_requests = - (get_unconfirmed_spend_requests(&wallet_dir)?).unwrap_or_default(); - let watchonly_wallet = WatchOnlyWallet::load_from(&wallet_dir, key.main_pubkey())?; - - Ok(Self { - key, - watchonly_wallet, - unconfirmed_spend_requests, - authentication_manager: AuthenticationManager::new(wallet_dir), - }) - } - - /// Loads a serialized wallet from a path. - pub fn load_from(root_dir: &Path) -> Result { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - Self::load_from_path(&wallet_dir, None) - } - - /// Tries to loads a serialized wallet from a path, bailing out if it doesn't exist. - pub fn try_load_from(root_dir: &Path) -> Result { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - Self::load_from_path_and_key(&wallet_dir, None, None) - } - - /// Loads a serialized wallet from a given path, no additional element will - /// be added to the provided path and strictly taken as the wallet files location. - pub fn load_from_path(wallet_dir: &Path, main_key: Option) -> Result { - std::fs::create_dir_all(wallet_dir)?; - Self::load_from_path_and_key(wallet_dir, main_key, None) - } - - /// Loads an encrypted serialized wallet from a given root path. - pub fn load_encrypted_from_path(root_dir: &Path, password: String) -> Result { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - std::fs::create_dir_all(&wallet_dir)?; - Self::load_from_path_and_key(&wallet_dir, None, Some(password)) - } - - pub fn address(&self) -> MainPubkey { - self.key.main_pubkey() - } - - pub fn unconfirmed_spend_requests(&self) -> &BTreeSet { - &self.unconfirmed_spend_requests - } - - pub fn unconfirmed_spend_requests_mut(&mut self) -> &mut BTreeSet { - &mut self.unconfirmed_spend_requests - } - - /// Moves all files for the current wallet, including keys and cashnotes - /// to directory root_dir/wallet_ADDRESS - pub fn stash(root_dir: &Path) -> Result { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - let wallet_pub_key = - get_main_pubkey(&wallet_dir)?.ok_or(Error::PubkeyNotFound(wallet_dir.clone()))?; - let addr_hex = wallet_pub_key.to_hex(); - let new_name = format!("{WALLET_DIR_NAME}_{addr_hex}"); - let moved_dir = root_dir.join(new_name); - std::fs::rename(wallet_dir, &moved_dir)?; - Ok(moved_dir) - } - - /// Moves a previously stashed wallet to the root wallet directory. - pub fn unstash(root_dir: &Path, addr_hex: &str) -> Result<()> { - let cleared_name = format!("{WALLET_DIR_NAME}_{addr_hex}"); - let cleared_dir = root_dir.join(cleared_name); - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - - // Stash old wallet if it exists - if wallet_dir.exists() { - if let Ok(_wallet) = HotWallet::load_from(root_dir) { - Self::stash(root_dir)?; - } - - std::fs::remove_dir_all(&wallet_dir)?; - } - - std::fs::rename(cleared_dir, wallet_dir)?; - Ok(()) - } - - /// Removes all files for the current wallet, including keys and cashnotes - pub fn remove(root_dir: &Path) -> Result<()> { - let wallet_dir = root_dir.join(WALLET_DIR_NAME); - std::fs::remove_dir_all(wallet_dir)?; - Ok(()) - } - - /// To remove a specific spend from the requests, if eg, we see one spend is _bad_ - pub fn clear_specific_spend_request(&mut self, unique_pub_key: UniquePubkey) { - if let Err(error) = self.remove_cash_notes_from_disk(vec![&unique_pub_key]) { - warn!("Could not clean spend {unique_pub_key:?} due to {error:?}"); - } - - self.unconfirmed_spend_requests - .retain(|signed_spend| signed_spend.spend.unique_pubkey.ne(&unique_pub_key)) - } - - /// Once spends are verified we can clear them and clean up - pub fn clear_confirmed_spend_requests(&mut self) { - if let Err(error) = self.remove_cash_notes_from_disk( - self.unconfirmed_spend_requests - .iter() - .map(|s| &s.spend.unique_pubkey), - ) { - warn!("Could not clean confirmed spent cash_notes due to {error:?}"); - } - - // Also need to remove unconfirmed_spend_requests from disk if was pre-loaded. - let _ = self.remove_unconfirmed_spend_requests(); - - self.unconfirmed_spend_requests = Default::default(); - } - - pub fn balance(&self) -> NanoTokens { - self.watchonly_wallet.balance() - } - - pub fn sign(&self, unsigned_tx: UnsignedTransaction) -> Result { - if let Err(err) = unsigned_tx.verify() { - return Err(Error::CouldNotSignTransaction(format!( - "Failed to verify unsigned transaction: {err:?}" - ))); - } - let signed_tx = unsigned_tx - .sign(&self.key) - .map_err(|e| Error::CouldNotSignTransaction(e.to_string()))?; - if let Err(err) = signed_tx.verify() { - return Err(Error::CouldNotSignTransaction(format!( - "Failed to verify signed transaction: {err:?}" - ))); - } - Ok(signed_tx) - } - - /// Checks whether the specified cash_note already presents - pub fn cash_note_presents(&mut self, id: &UniquePubkey) -> bool { - self.watchonly_wallet - .available_cash_notes() - .contains_key(id) - } - - /// Returns all available cash_notes and an exclusive access to the wallet so no concurrent processes can - /// get available cash_notes while we're modifying the wallet - /// once the updated wallet is stored to disk it is safe to drop the WalletExclusiveAccess - pub fn available_cash_notes(&mut self) -> Result<(Vec, WalletExclusiveAccess)> { - trace!("Trying to lock wallet to get available cash_notes..."); - // lock and load from disk to make sure we're up to date and others can't modify the wallet concurrently - let exclusive_access = self.lock()?; - self.reload()?; - trace!("Wallet locked and loaded!"); - - // get the available cash_notes - let mut available_cash_notes = vec![]; - let wallet_dir = self.watchonly_wallet.wallet_dir().to_path_buf(); - for (id, _token) in self.watchonly_wallet.available_cash_notes().iter() { - let held_cash_note = load_created_cash_note(id, &wallet_dir); - if let Some(cash_note) = held_cash_note { - if cash_note.derived_key(&self.key).is_ok() { - available_cash_notes.push(cash_note.clone()); - } else { - warn!( - "Skipping CashNote {:?} because we don't have the key to spend it", - cash_note.unique_pubkey() - ); - } - } else { - warn!("Skipping CashNote {:?} because we don't have it", id); - } - } - - Ok((available_cash_notes, exclusive_access)) - } - - /// Remove the payment_details of the given XorName from disk. - pub fn remove_payment_for_xorname(&self, name: &XorName) { - self.api().remove_payment_transaction(name) - } - - pub fn build_unsigned_transaction( - &mut self, - to: Vec<(NanoTokens, MainPubkey)>, - reason: Option, - ) -> Result { - self.watchonly_wallet.build_unsigned_transaction(to, reason) - } - - /// Make a transfer and return all created cash_notes - pub fn local_send( - &mut self, - to: Vec<(NanoTokens, MainPubkey)>, - reason: Option, - ) -> Result> { - let mut rng = &mut rand::rngs::OsRng; - // create a unique key for each output - let to_unique_keys: Vec<_> = to - .into_iter() - .map(|(amount, address)| (amount, address, DerivationIndex::random(&mut rng), false)) - .collect(); - - let (available_cash_notes, exclusive_access) = self.available_cash_notes()?; - println!("Available CashNotes for local send: {available_cash_notes:#?}"); - - let reason = reason.unwrap_or_default(); - - let signed_tx = SignedTransaction::new( - available_cash_notes, - to_unique_keys, - self.address(), - reason, - &self.key, - )?; - - let created_cash_notes = signed_tx.output_cashnotes.clone(); - - self.update_local_wallet(signed_tx, exclusive_access, true)?; - - trace!("Releasing wallet lock"); // by dropping _exclusive_access - Ok(created_cash_notes) - } - - // Create SignedSpends directly to forward all accumulated balance to the receipient. - #[cfg(feature = "reward-forward")] - pub fn prepare_forward_signed_spend( - &mut self, - to: Vec<(NanoTokens, MainPubkey)>, - reward_tracking_reason: String, - ) -> Result> { - let (available_cash_notes, exclusive_access) = self.available_cash_notes()?; - debug!( - "Available CashNotes for local send: {:#?}", - available_cash_notes - ); - - let spend_reason = match SpendReason::create_reward_tracking_reason(&reward_tracking_reason) - { - Ok(spend_reason) => spend_reason, - Err(err) => { - error!("Failed to generate spend_reason {err:?}"); - return Err(Error::CouldNotSendMoney(format!( - "Failed to generate spend_reason {err:?}" - ))); - } - }; - - // create a unique key for each output - let mut rng = &mut rand::rngs::OsRng; - let to_unique_keys: Vec<_> = to - .into_iter() - .map(|(amount, address)| (amount, address, DerivationIndex::random(&mut rng), false)) - .collect(); - - let signed_tx = SignedTransaction::new( - available_cash_notes, - to_unique_keys, - self.address(), - spend_reason, - &self.key, - )?; - let signed_spends: Vec<_> = signed_tx.spends.iter().cloned().collect(); - - self.update_local_wallet(signed_tx, exclusive_access, false)?; - - // cash_notes better to be removed from disk - let _ = - self.remove_cash_notes_from_disk(signed_spends.iter().map(|s| &s.spend.unique_pubkey)); - - // signed_spends need to be flushed to the disk as confirmed_spends as well. - let ss_btree: BTreeSet<_> = signed_spends.iter().cloned().collect(); - let _ = remove_unconfirmed_spend_requests(self.watchonly_wallet.wallet_dir(), &ss_btree); - - Ok(signed_spends) - } - - /// Performs a payment for each content address. - /// Includes payment of network royalties. - /// Returns the amount paid for storage, including the network royalties fee paid. - pub fn local_send_storage_payment( - &mut self, - price_map: &BTreeMap)>, - ) -> Result<(NanoTokens, NanoTokens)> { - let mut rng = &mut rand::thread_rng(); - let mut storage_cost = NanoTokens::zero(); - let mut royalties_fees = NanoTokens::zero(); - - let start = Instant::now(); - - // create random derivation indexes for recipients - let mut recipients_by_xor = BTreeMap::new(); - for (xorname, (main_pubkey, quote, peer_id_bytes)) in price_map.iter() { - let storage_payee = ( - quote.cost, - *main_pubkey, - DerivationIndex::random(&mut rng), - peer_id_bytes.clone(), - ); - let royalties_fee = calculate_royalties_fee(quote.cost); - let royalties_payee = ( - royalties_fee, - *NETWORK_ROYALTIES_PK, - DerivationIndex::random(&mut rng), - ); - - storage_cost = storage_cost - .checked_add(quote.cost) - .ok_or(WalletError::TotalPriceTooHigh)?; - royalties_fees = royalties_fees - .checked_add(royalties_fee) - .ok_or(WalletError::TotalPriceTooHigh)?; - - recipients_by_xor.insert(xorname, (storage_payee, royalties_payee)); - } - - // create offline transfers - let recipients = recipients_by_xor - .values() - .flat_map(|(node, roy)| { - vec![(node.0, node.1, node.2, false), (roy.0, roy.1, roy.2, true)] - }) - .collect(); - - trace!( - "local_send_storage_payment prepared in {:?}", - start.elapsed() - ); - - let start = Instant::now(); - let (available_cash_notes, exclusive_access) = self.available_cash_notes()?; - trace!( - "local_send_storage_payment fetched {} cashnotes in {:?}", - available_cash_notes.len(), - start.elapsed() - ); - debug!("Available CashNotes: {:#?}", available_cash_notes); - - let spend_reason = Default::default(); - let start = Instant::now(); - let signed_tx = SignedTransaction::new( - available_cash_notes, - recipients, - self.address(), - spend_reason, - &self.key, - )?; - trace!( - "local_send_storage_payment created offline_transfer with {} cashnotes in {:?}", - signed_tx.output_cashnotes.len(), - start.elapsed() - ); - - let start = Instant::now(); - // cache transfer payments in the wallet - let mut cashnotes_to_use: HashSet = - signed_tx.output_cashnotes.iter().cloned().collect(); - for (xorname, recipients_info) in recipients_by_xor { - let (storage_payee, royalties_payee) = recipients_info; - let (pay_amount, node_key, _, peer_id_bytes) = storage_payee; - let cash_note_for_node = cashnotes_to_use - .iter() - .find(|cash_note| { - cash_note.value() == pay_amount && cash_note.main_pubkey() == &node_key - }) - .ok_or(Error::CouldNotSendMoney(format!( - "No cashnote found to pay node for {xorname:?}" - )))? - .clone(); - cashnotes_to_use.remove(&cash_note_for_node); - let transfer_amount = cash_note_for_node.value(); - let transfer_for_node = Transfer::transfer_from_cash_note(&cash_note_for_node)?; - trace!("Created transaction regarding {xorname:?} paying {transfer_amount:?} to {node_key:?}."); - - let royalties_key = royalties_payee.1; - let royalties_amount = royalties_payee.0; - let cash_note_for_royalties = cashnotes_to_use - .iter() - .find(|cash_note| { - cash_note.value() == royalties_amount - && cash_note.main_pubkey() == &royalties_key - }) - .ok_or(Error::CouldNotSendMoney(format!( - "No cashnote found to pay royalties for {xorname:?}" - )))? - .clone(); - cashnotes_to_use.remove(&cash_note_for_royalties); - let royalties = Transfer::royalties_transfer_from_cash_note(&cash_note_for_royalties)?; - let royalties_amount = cash_note_for_royalties.value(); - trace!("Created network royalties cnr regarding {xorname:?} paying {royalties_amount:?} to {royalties_key:?}."); - - let quote = price_map - .get(xorname) - .ok_or(Error::CouldNotSendMoney(format!( - "No quote found for {xorname:?}" - )))? - .1 - .clone(); - let payment = PaymentDetails { - recipient: node_key, - peer_id_bytes, - transfer: (transfer_for_node, transfer_amount), - royalties: (royalties, royalties_amount), - quote, - }; - - let _ = self - .watchonly_wallet - .insert_payment_transaction(*xorname, payment); - } - trace!( - "local_send_storage_payment completed payments insertion in {:?}", - start.elapsed() - ); - - // write all changes to local wallet - let start = Instant::now(); - self.update_local_wallet(signed_tx, exclusive_access, true)?; - trace!( - "local_send_storage_payment completed local wallet update in {:?}", - start.elapsed() - ); - - Ok((storage_cost, royalties_fees)) - } - - #[cfg(feature = "test-utils")] - pub fn test_update_local_wallet( - &mut self, - transfer: SignedTransaction, - exclusive_access: WalletExclusiveAccess, - insert_into_pending_spends: bool, - ) -> Result<()> { - self.update_local_wallet(transfer, exclusive_access, insert_into_pending_spends) - } - - fn update_local_wallet( - &mut self, - signed_tx: SignedTransaction, - exclusive_access: WalletExclusiveAccess, - insert_into_pending_spends: bool, - ) -> Result<()> { - // First of all, update client local state. - let spent_unique_pubkeys: BTreeSet<_> = - signed_tx.spends.iter().map(|s| s.unique_pubkey()).collect(); - - self.watchonly_wallet - .mark_notes_as_spent(spent_unique_pubkeys.clone()); - - if let Some(cash_note) = signed_tx.change_cashnote { - let start = Instant::now(); - self.watchonly_wallet.deposit(&[cash_note.clone()])?; - trace!( - "update_local_wallet completed deposit change cash_note in {:?}", - start.elapsed() - ); - let start = Instant::now(); - - // Only the change_cash_note, i.e. the pay-in one, needs to be stored to disk. - // - // Paying out cash_note doesn't need to be stored into disk. - // As it is the transfer, that generated from it, to be sent out to network, - // and be stored within the unconfirmed_spends, and to be re-sent in case of failure. - self.store_cash_notes_to_disk(&[cash_note])?; - trace!( - "update_local_wallet completed store change cash_note to disk in {:?}", - start.elapsed() - ); - } - if insert_into_pending_spends { - for request in signed_tx.spends { - self.unconfirmed_spend_requests.insert(request); - } - } - - // store wallet to disk - let start = Instant::now(); - self.store(exclusive_access)?; - trace!( - "update_local_wallet completed store self wallet to disk in {:?}", - start.elapsed() - ); - Ok(()) - } - - /// Deposit the given cash_notes on the wallet (without storing them to disk). - pub fn deposit(&mut self, received_cash_notes: &Vec) -> Result<()> { - self.watchonly_wallet.deposit(received_cash_notes) - } - - /// Store the given cash_notes to the `cash_notes` dir in the wallet dir. - /// Update and store the updated wallet to disk - /// This function locks the wallet to prevent concurrent processes from writing to it - pub fn deposit_and_store_to_disk(&mut self, received_cash_notes: &Vec) -> Result<()> { - self.watchonly_wallet - .deposit_and_store_to_disk(received_cash_notes) - } - - pub fn unwrap_transfer(&self, transfer: &Transfer) -> Result> { - transfer - .cashnote_redemptions(&self.key) - .map_err(|_| Error::FailedToDecypherTransfer) - } - - pub fn derive_key(&self, derivation_index: &DerivationIndex) -> DerivedSecretKey { - self.key.derive_key(derivation_index) - } - - /// Loads a serialized wallet from a path. - // TODO: what's the behaviour here if path has stored key and we pass one in? - fn load_from_path_and_key( - wallet_dir: &Path, - main_key: Option, - main_key_password: Option, - ) -> Result { - let key = match get_main_key_from_disk(wallet_dir, main_key_password.to_owned()) { - Ok(key) => { - if let Some(passed_key) = main_key { - if key.secret_key() != passed_key.secret_key() { - warn!("main_key passed to load_from_path_and_key, but a key was found in the wallet dir. Using the one found in the wallet dir."); - } - } - - key - } - Err(error) => { - if let Some(key) = main_key { - store_new_keypair(wallet_dir, &key, main_key_password)?; - key - } else { - error!( - "No main key found when loading wallet from path {:?}", - wallet_dir - ); - - return Err(error); - } - } - }; - let unconfirmed_spend_requests = - (get_unconfirmed_spend_requests(wallet_dir)?).unwrap_or_default(); - let watchonly_wallet = WatchOnlyWallet::load_from(wallet_dir, key.main_pubkey())?; - - Ok(Self { - key, - watchonly_wallet, - unconfirmed_spend_requests, - authentication_manager: AuthenticationManager::new(wallet_dir.to_path_buf()), - }) - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use super::HotWallet; - use crate::wallet::authentication::AuthenticationManager; - use crate::{ - genesis::{create_first_cash_note_from_key, GENESIS_CASHNOTE_AMOUNT}, - wallet::{ - data_payments::PaymentQuote, hot_wallet::WALLET_DIR_NAME, wallet_file::store_wallet, - watch_only::WatchOnlyWallet, KeyLessWallet, - }, - MainSecretKey, NanoTokens, SpendAddress, - }; - use assert_fs::TempDir; - use eyre::Result; - use xor_name::XorName; - - #[tokio::test] - async fn keyless_wallet_to_and_from_file() -> Result<()> { - let key = MainSecretKey::random(); - let mut wallet = KeyLessWallet::default(); - let genesis = create_first_cash_note_from_key(&key).expect("Genesis creation to succeed."); - - let dir = create_temp_dir(); - let wallet_dir = dir.path().to_path_buf(); - - wallet - .available_cash_notes - .insert(genesis.unique_pubkey(), genesis.value()); - - store_wallet(&wallet_dir, &wallet)?; - - let deserialized = - KeyLessWallet::load_from(&wallet_dir)?.expect("There to be a wallet on disk."); - - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deserialized.balance().as_nano()); - - Ok(()) - } - - #[test] - fn wallet_basics() -> Result<()> { - let key = MainSecretKey::random(); - let main_pubkey = key.main_pubkey(); - let dir = create_temp_dir(); - - let deposit_only = HotWallet { - key, - watchonly_wallet: WatchOnlyWallet::new(main_pubkey, &dir, KeyLessWallet::default()), - unconfirmed_spend_requests: Default::default(), - authentication_manager: AuthenticationManager::new(dir.to_path_buf()), - }; - - assert_eq!(main_pubkey, deposit_only.address()); - assert_eq!(NanoTokens::zero(), deposit_only.balance()); - - assert!(deposit_only - .watchonly_wallet - .available_cash_notes() - .is_empty()); - - Ok(()) - } - - /// ----------------------------------- - /// <-------> DepositWallet <---------> - /// ----------------------------------- - - #[tokio::test] - async fn deposit_empty_list_does_nothing() -> Result<()> { - let key = MainSecretKey::random(); - let main_pubkey = key.main_pubkey(); - let dir = create_temp_dir(); - - let mut deposit_only = HotWallet { - key, - watchonly_wallet: WatchOnlyWallet::new(main_pubkey, &dir, KeyLessWallet::default()), - unconfirmed_spend_requests: Default::default(), - authentication_manager: AuthenticationManager::new(dir.to_path_buf()), - }; - - deposit_only.deposit_and_store_to_disk(&vec![])?; - - assert_eq!(NanoTokens::zero(), deposit_only.balance()); - - assert!(deposit_only - .watchonly_wallet - .available_cash_notes() - .is_empty()); - - Ok(()) - } - - #[tokio::test] - async fn deposit_adds_cash_notes_that_belongs_to_the_wallet() -> Result<()> { - let key = MainSecretKey::random(); - let main_pubkey = key.main_pubkey(); - let genesis = create_first_cash_note_from_key(&key).expect("Genesis creation to succeed."); - let dir = create_temp_dir(); - - let mut deposit_only = HotWallet { - key, - watchonly_wallet: WatchOnlyWallet::new(main_pubkey, &dir, KeyLessWallet::default()), - unconfirmed_spend_requests: Default::default(), - authentication_manager: AuthenticationManager::new(dir.to_path_buf()), - }; - - deposit_only.deposit_and_store_to_disk(&vec![genesis])?; - - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deposit_only.balance().as_nano()); - - Ok(()) - } - - #[tokio::test] - async fn deposit_does_not_add_cash_notes_not_belonging_to_the_wallet() -> Result<()> { - let key = MainSecretKey::random(); - let main_pubkey = key.main_pubkey(); - let genesis = create_first_cash_note_from_key(&MainSecretKey::random()) - .expect("Genesis creation to succeed."); - let dir = create_temp_dir(); - - let mut local_wallet = HotWallet { - key, - watchonly_wallet: WatchOnlyWallet::new(main_pubkey, &dir, KeyLessWallet::default()), - unconfirmed_spend_requests: Default::default(), - authentication_manager: AuthenticationManager::new(dir.to_path_buf()), - }; - - local_wallet.deposit_and_store_to_disk(&vec![genesis])?; - - assert_eq!(NanoTokens::zero(), local_wallet.balance()); - - Ok(()) - } - - #[tokio::test] - async fn deposit_is_idempotent() -> Result<()> { - let key = MainSecretKey::random(); - let main_pubkey = key.main_pubkey(); - let genesis_0 = - create_first_cash_note_from_key(&key).expect("Genesis creation to succeed."); - let genesis_1 = - create_first_cash_note_from_key(&key).expect("Genesis creation to succeed."); - let dir = create_temp_dir(); - - let mut deposit_only = HotWallet { - key, - watchonly_wallet: WatchOnlyWallet::new(main_pubkey, &dir, KeyLessWallet::default()), - unconfirmed_spend_requests: Default::default(), - authentication_manager: AuthenticationManager::new(dir.to_path_buf()), - }; - - deposit_only.deposit_and_store_to_disk(&vec![genesis_0.clone()])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deposit_only.balance().as_nano()); - - deposit_only.deposit_and_store_to_disk(&vec![genesis_0])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deposit_only.balance().as_nano()); - - deposit_only.deposit_and_store_to_disk(&vec![genesis_1])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deposit_only.balance().as_nano()); - - Ok(()) - } - - #[tokio::test] - async fn deposit_wallet_to_and_from_file() -> Result<()> { - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - - let new_wallet = MainSecretKey::random(); - let mut depositor = HotWallet::create_from_key(&root_dir, new_wallet, None)?; - let genesis = - create_first_cash_note_from_key(&depositor.key).expect("Genesis creation to succeed."); - depositor.deposit_and_store_to_disk(&vec![genesis])?; - - let deserialized = HotWallet::load_from(&root_dir)?; - - assert_eq!(depositor.address(), deserialized.address()); - assert_eq!(GENESIS_CASHNOTE_AMOUNT, depositor.balance().as_nano()); - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deserialized.balance().as_nano()); - - assert_eq!(1, depositor.watchonly_wallet.available_cash_notes().len()); - - assert_eq!( - 1, - deserialized.watchonly_wallet.available_cash_notes().len() - ); - - let a_available = depositor - .watchonly_wallet - .available_cash_notes() - .values() - .last() - .expect("There to be an available CashNote."); - let b_available = deserialized - .watchonly_wallet - .available_cash_notes() - .values() - .last() - .expect("There to be an available CashNote."); - assert_eq!(a_available, b_available); - - Ok(()) - } - - /// -------------------------------- - /// <-------> SendWallet <---------> - /// -------------------------------- - - #[tokio::test] - async fn sending_decreases_balance() -> Result<()> { - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - let new_wallet = MainSecretKey::random(); - let mut sender = HotWallet::create_from_key(&root_dir, new_wallet, None)?; - let sender_cash_note = - create_first_cash_note_from_key(&sender.key).expect("Genesis creation to succeed."); - sender.deposit_and_store_to_disk(&vec![sender_cash_note])?; - - assert_eq!(GENESIS_CASHNOTE_AMOUNT, sender.balance().as_nano()); - - // We send to a new address. - let send_amount = 100; - let recipient_key = MainSecretKey::random(); - let recipient_main_pubkey = recipient_key.main_pubkey(); - let to = vec![(NanoTokens::from(send_amount), recipient_main_pubkey)]; - let created_cash_notes = sender.local_send(to, None)?; - - assert_eq!(1, created_cash_notes.len()); - assert_eq!( - GENESIS_CASHNOTE_AMOUNT - send_amount, - sender.balance().as_nano() - ); - - let recipient_cash_note = &created_cash_notes[0]; - assert_eq!(NanoTokens::from(send_amount), recipient_cash_note.value()); - assert_eq!(&recipient_main_pubkey, recipient_cash_note.main_pubkey()); - - Ok(()) - } - - #[tokio::test] - async fn send_wallet_to_and_from_file() -> Result<()> { - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - - let new_wallet = MainSecretKey::random(); - let mut sender = HotWallet::create_from_key(&root_dir, new_wallet, None)?; - - let sender_cash_note = - create_first_cash_note_from_key(&sender.key).expect("Genesis creation to succeed."); - sender.deposit_and_store_to_disk(&vec![sender_cash_note])?; - - // We send to a new address. - let send_amount = 100; - let recipient_key = MainSecretKey::random(); - let recipient_main_pubkey = recipient_key.main_pubkey(); - let to = vec![(NanoTokens::from(send_amount), recipient_main_pubkey)]; - let _created_cash_notes = sender.local_send(to, None)?; - - let deserialized = HotWallet::load_from(&root_dir)?; - - assert_eq!(sender.address(), deserialized.address()); - assert_eq!( - GENESIS_CASHNOTE_AMOUNT - send_amount, - sender.balance().as_nano() - ); - assert_eq!( - GENESIS_CASHNOTE_AMOUNT - send_amount, - deserialized.balance().as_nano() - ); - - assert_eq!(1, sender.watchonly_wallet.available_cash_notes().len()); - - assert_eq!( - 1, - deserialized.watchonly_wallet.available_cash_notes().len() - ); - - let a_available = sender - .watchonly_wallet - .available_cash_notes() - .values() - .last() - .expect("There to be an available CashNote."); - let b_available = deserialized - .watchonly_wallet - .available_cash_notes() - .values() - .last() - .expect("There to be an available CashNote."); - assert_eq!(a_available, b_available); - - Ok(()) - } - - #[tokio::test] - async fn store_created_cash_note_gives_file_that_try_load_cash_notes_can_use() -> Result<()> { - let sender_root_dir = create_temp_dir(); - let sender_root_dir = sender_root_dir.path().to_path_buf(); - let new_wallet = MainSecretKey::random(); - let mut sender = HotWallet::create_from_key(&sender_root_dir, new_wallet, None)?; - - let sender_cash_note = - create_first_cash_note_from_key(&sender.key).expect("Genesis creation to succeed."); - sender.deposit_and_store_to_disk(&vec![sender_cash_note])?; - - let send_amount = 100; - - // Send to a new address. - let recipient_root_dir = create_temp_dir(); - let recipient_root_dir = recipient_root_dir.path().to_path_buf(); - - let new_wallet = MainSecretKey::random(); - let mut recipient = HotWallet::create_from_key(&recipient_root_dir, new_wallet, None)?; - - let recipient_main_pubkey = recipient.key.main_pubkey(); - - let to = vec![(NanoTokens::from(send_amount), recipient_main_pubkey)]; - let created_cash_notes = sender.local_send(to, None)?; - let cash_note = created_cash_notes[0].clone(); - let unique_pubkey = cash_note.unique_pubkey(); - sender.store_cash_notes_to_disk(&[cash_note])?; - - let unique_pubkey_name = *SpendAddress::from_unique_pubkey(&unique_pubkey).xorname(); - let unique_pubkey_file_name = format!("{}.cash_note", hex::encode(unique_pubkey_name)); - - let created_cash_notes_dir = sender_root_dir.join(WALLET_DIR_NAME).join("cash_notes"); - let created_cash_note_file = created_cash_notes_dir.join(&unique_pubkey_file_name); - - let received_cash_note_dir = recipient_root_dir.join(WALLET_DIR_NAME).join("cash_notes"); - - std::fs::create_dir_all(&received_cash_note_dir)?; - let received_cash_note_file = received_cash_note_dir.join(&unique_pubkey_file_name); - - // Move the created cash_note to the recipient's received_cash_notes dir. - std::fs::rename(created_cash_note_file, received_cash_note_file)?; - - assert_eq!(0, recipient.balance().as_nano()); - - recipient.try_load_cash_notes()?; - - assert_eq!(1, recipient.watchonly_wallet.available_cash_notes().len()); - - let available = recipient - .watchonly_wallet - .available_cash_notes() - .keys() - .last() - .expect("There to be an available CashNote."); - - assert_eq!(available, &unique_pubkey); - assert_eq!(send_amount, recipient.balance().as_nano()); - - Ok(()) - } - - #[tokio::test] - async fn test_local_send_storage_payment_returns_correct_cost() -> Result<()> { - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - - let new_wallet = MainSecretKey::random(); - let mut sender = HotWallet::create_from_key(&root_dir, new_wallet, None)?; - - let sender_cash_note = - create_first_cash_note_from_key(&sender.key).expect("Genesis creation to succeed."); - sender.deposit_and_store_to_disk(&vec![sender_cash_note])?; - - let mut rng = bls::rand::thread_rng(); - let xor1 = XorName::random(&mut rng); - let xor2 = XorName::random(&mut rng); - let xor3 = XorName::random(&mut rng); - let xor4 = XorName::random(&mut rng); - - let key1a = MainSecretKey::random().main_pubkey(); - let key2a = MainSecretKey::random().main_pubkey(); - let key3a = MainSecretKey::random().main_pubkey(); - let key4a = MainSecretKey::random().main_pubkey(); - - let map = BTreeMap::from([ - ( - xor1, - (key1a, PaymentQuote::test_dummy(xor1, 100.into()), vec![]), - ), - ( - xor2, - (key2a, PaymentQuote::test_dummy(xor2, 200.into()), vec![]), - ), - ( - xor3, - (key3a, PaymentQuote::test_dummy(xor3, 300.into()), vec![]), - ), - ( - xor4, - (key4a, PaymentQuote::test_dummy(xor4, 400.into()), vec![]), - ), - ]); - - let (price, _) = sender.local_send_storage_payment(&map)?; - - let expected_price: u64 = map.values().map(|(_, quote, _)| quote.cost.as_nano()).sum(); - assert_eq!(price.as_nano(), expected_price); - - Ok(()) - } - - /// -------------------------------- - /// <-------> Encryption <---------> - /// -------------------------------- - - #[test] - fn test_encrypting_existing_unencrypted_wallet() -> Result<()> { - let password: &'static str = "safenetwork"; - let wrong_password: &'static str = "unsafenetwork"; - - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - let wallet_key = MainSecretKey::random(); - - let unencrypted_wallet = HotWallet::create_from_key(&root_dir, wallet_key, None)?; - - HotWallet::encrypt(&root_dir, password)?; - - let mut encrypted_wallet = - HotWallet::load_encrypted_from_path(&root_dir, password.to_owned())?; - - // Should fail when not authenticated with password yet - assert!(encrypted_wallet.authenticate().is_err()); - - // Authentication should fail with wrong password - assert!(encrypted_wallet - .authenticate_with_password(wrong_password.to_owned()) - .is_err()); - - encrypted_wallet.authenticate_with_password(password.to_owned())?; - - encrypted_wallet.reload()?; - - assert_eq!(encrypted_wallet.address(), unencrypted_wallet.address()); - - Ok(()) - } - - /// -------------------------------- - /// <-------> Other <---------> - /// -------------------------------- - - #[test] - fn test_stashing_and_unstashing() -> Result<()> { - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - let wallet_key = MainSecretKey::random(); - let wallet = HotWallet::create_from_key(&root_dir, wallet_key, None)?; - let pub_key_hex_str = wallet.address().to_hex(); - - // Stash wallet - HotWallet::stash(&root_dir)?; - - // There should be no active wallet now - assert!(HotWallet::load_from(&root_dir).is_err()); - - // Unstash wallet - HotWallet::unstash(&root_dir, &pub_key_hex_str)?; - - let unstashed_wallet = HotWallet::load_from(&root_dir)?; - - assert_eq!(unstashed_wallet.address().to_hex(), pub_key_hex_str); - - Ok(()) - } - - fn create_temp_dir() -> TempDir { - TempDir::new().expect("Should be able to create a temp dir.") - } -} diff --git a/sn_transfers/src/wallet/keys.rs b/sn_transfers/src/wallet/keys.rs deleted file mode 100644 index 2e0bed01ba..0000000000 --- a/sn_transfers/src/wallet/keys.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::error::{Error, Result}; -use crate::wallet::encryption::{ - encrypt_secret_key, EncryptedSecretKey, ENCRYPTED_MAIN_SECRET_KEY_FILENAME, -}; -use crate::{MainPubkey, MainSecretKey}; -use hex::{decode, encode}; -use std::path::Path; - -/// Filename for storing the node's reward (BLS hex-encoded) main secret key. -const MAIN_SECRET_KEY_FILENAME: &str = "main_secret_key"; -/// Filename for storing the node's reward (BLS hex-encoded) public key. -const MAIN_PUBKEY_FILENAME: &str = "main_pubkey"; - -/// Writes the public address and main key (hex-encoded) to different locations at disk. -pub(crate) fn store_new_keypair( - wallet_dir: &Path, - main_key: &MainSecretKey, - password: Option, -) -> Result<()> { - store_new_pubkey(wallet_dir, &main_key.main_pubkey())?; - store_main_secret_key(wallet_dir, main_key, password)?; - - Ok(()) -} - -/// Returns sn_transfers::MainSecretKey or None if file doesn't exist. It assumes it's hex-encoded. -pub(super) fn get_main_key_from_disk( - wallet_dir: &Path, - password: Option, -) -> Result { - // If a valid `main_secret_key.encrypted` file is found, use it - if EncryptedSecretKey::file_exists(wallet_dir) { - let encrypted_secret_key = EncryptedSecretKey::from_file(wallet_dir)?; - let password = password.ok_or(Error::EncryptedMainSecretKeyRequiresPassword)?; - - encrypted_secret_key.decrypt(&password) - } else { - // Else try a `main_secret_key` file - let path = wallet_dir.join(MAIN_SECRET_KEY_FILENAME); - - if !path.is_file() { - return Err(Error::MainSecretKeyNotFound(path)); - } - - let secret_hex_bytes = std::fs::read(&path)?; - let secret = bls_secret_from_hex(secret_hex_bytes)?; - - Ok(MainSecretKey::new(secret)) - } -} - -/// Writes the main secret key (hex-encoded) to disk. -/// -/// When a password is set, the secret key file will be encrypted. -pub(crate) fn store_main_secret_key( - wallet_dir: &Path, - main_secret_key: &MainSecretKey, - password: Option, -) -> Result<()> { - // If encryption_password is provided, the secret key will be encrypted with the password - if let Some(password) = password.as_ref() { - let encrypted_key = encrypt_secret_key(main_secret_key, password)?; - // Save the encrypted secret key in `main_secret_key.encrypted` file - encrypted_key.save_to_file(wallet_dir)?; - } else { - // Save secret key as plain hex text in `main_secret_key` file - let secret_key_path = wallet_dir.join(MAIN_SECRET_KEY_FILENAME); - std::fs::write(secret_key_path, encode(main_secret_key.to_bytes()))?; - } - - Ok(()) -} - -/// Writes the public address (hex-encoded) to disk. -pub(crate) fn store_new_pubkey(wallet_dir: &Path, main_pubkey: &MainPubkey) -> Result<()> { - let public_key_path = wallet_dir.join(MAIN_PUBKEY_FILENAME); - std::fs::write(public_key_path, encode(main_pubkey.to_bytes())) - .map_err(|e| Error::FailedToHexEncodeKey(e.to_string()))?; - Ok(()) -} - -/// Returns Some(sn_transfers::MainPubkey) or None if file doesn't exist. It assumes it's hex-encoded. -pub(super) fn get_main_pubkey(wallet_dir: &Path) -> Result> { - let path = wallet_dir.join(MAIN_PUBKEY_FILENAME); - if !path.is_file() { - return Ok(None); - } - - let pk_hex_bytes = std::fs::read(&path)?; - let main_pk = MainPubkey::from_hex(pk_hex_bytes)?; - - Ok(Some(main_pk)) -} - -/// Delete the file containing the secret key `main_secret_key`. -/// WARNING: Only call this if you know what you're doing! -pub(crate) fn delete_unencrypted_main_secret_key(wallet_dir: &Path) -> Result<()> { - let path = wallet_dir.join(MAIN_SECRET_KEY_FILENAME); - std::fs::remove_file(path)?; - Ok(()) -} - -/// Delete the file containing the secret key `main_secret_key.encrypted`. -/// WARNING: Only call this if you know what you're doing! -pub(crate) fn delete_encrypted_main_secret_key(wallet_dir: &Path) -> Result<()> { - let path = wallet_dir.join(ENCRYPTED_MAIN_SECRET_KEY_FILENAME); - std::fs::remove_file(path)?; - Ok(()) -} - -/// Construct a BLS secret key from a hex-encoded string. -pub fn bls_secret_from_hex>(hex: T) -> Result { - let bytes = decode(hex).map_err(|_| Error::FailedToDecodeHexToKey)?; - let bytes_fixed_len: [u8; bls::SK_SIZE] = bytes - .as_slice() - .try_into() - .map_err(|_| Error::FailedToParseBlsKey)?; - let sk = bls::SecretKey::from_bytes(bytes_fixed_len)?; - Ok(sk) -} - -#[cfg(test)] -mod test { - use super::{get_main_key_from_disk, store_new_keypair, MainSecretKey}; - use assert_fs::TempDir; - use eyre::Result; - - #[test] - fn reward_key_to_and_from_file() -> Result<()> { - let main_key = MainSecretKey::random(); - let dir = create_temp_dir(); - let root_dir = dir.path().to_path_buf(); - store_new_keypair(&root_dir, &main_key, None)?; - let secret_result = get_main_key_from_disk(&root_dir, None)?; - assert_eq!(secret_result.main_pubkey(), main_key.main_pubkey()); - Ok(()) - } - - fn create_temp_dir() -> TempDir { - TempDir::new().expect("Should be able to create a temp dir.") - } -} diff --git a/sn_transfers/src/wallet/wallet_file.rs b/sn_transfers/src/wallet/wallet_file.rs deleted file mode 100644 index d09109821c..0000000000 --- a/sn_transfers/src/wallet/wallet_file.rs +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::{ - error::{Error, Result}, - KeyLessWallet, -}; -use crate::{CashNote, SignedSpend, SpendAddress, UniquePubkey}; -use serde::Serialize; -use std::{ - collections::BTreeSet, - fs, - path::{Path, PathBuf}, -}; - -// Filename for storing a wallet. -const WALLET_FILE_NAME: &str = "wallet"; -const WALLET_LOCK_FILE_NAME: &str = "wallet.lock"; -const CASHNOTES_DIR_NAME: &str = "cash_notes"; -const UNCONFIRMED_TX_NAME: &str = "unconfirmed_spend_requests"; -const CONFIRMED_SPENDS_DIR_NAME: &str = "confirmed_spends"; - -/// Writes the `KeyLessWallet` to the specified path. -pub(super) fn store_wallet(wallet_dir: &Path, wallet: &KeyLessWallet) -> Result<()> { - let wallet_path = wallet_dir.join(WALLET_FILE_NAME); - let mut file = fs::File::create(wallet_path)?; - let mut serialiser = rmp_serde::encode::Serializer::new(&mut file); - wallet.serialize(&mut serialiser)?; - Ok(()) -} - -/// Returns the wallet filename -pub(super) fn wallet_file_name(wallet_dir: &Path) -> PathBuf { - wallet_dir.join(WALLET_FILE_NAME) -} - -/// Returns the wallet lockfile filename -pub fn wallet_lockfile_name(wallet_dir: &Path) -> PathBuf { - wallet_dir.join(WALLET_LOCK_FILE_NAME) -} - -/// Writes the `unconfirmed_spend_requests` to the specified path. -pub(super) fn store_unconfirmed_spend_requests( - wallet_dir: &Path, - unconfirmed_spend_requests: &BTreeSet, -) -> Result<()> { - let unconfirmed_spend_requests_path = wallet_dir.join(UNCONFIRMED_TX_NAME); - - let mut file = fs::File::create(unconfirmed_spend_requests_path)?; - let mut serialiser = rmp_serde::encode::Serializer::new(&mut file); - unconfirmed_spend_requests.serialize(&mut serialiser)?; - Ok(()) -} - -/// Remove the `unconfirmed_spend_requests` from the specified path. -pub(super) fn remove_unconfirmed_spend_requests( - wallet_dir: &Path, - unconfirmed_spend_requests: &BTreeSet, -) -> Result<()> { - // Flush out spends to dedicated dir first - let spends_dir = wallet_dir.join(CONFIRMED_SPENDS_DIR_NAME); - fs::create_dir_all(&spends_dir)?; - for spend in unconfirmed_spend_requests.iter() { - let spend_hex_name = spend.address().to_hex(); - let spend_file_path = spends_dir.join(&spend_hex_name); - debug!("Writing confirmed_spend instance to: {spend_file_path:?}"); - fs::write(spend_file_path, spend.to_bytes())?; - } - - let unconfirmed_spend_requests_path = wallet_dir.join(UNCONFIRMED_TX_NAME); - - debug!("Removing unconfirmed_spend_requests from {unconfirmed_spend_requests_path:?}"); - fs::remove_file(unconfirmed_spend_requests_path)?; - Ok(()) -} - -/// Returns `Some(SignedSpend)` or None if spend doesn't exist. -pub(super) fn get_confirmed_spend( - wallet_dir: &Path, - spend_addr: SpendAddress, -) -> Result> { - let spends_dir = wallet_dir.join(CONFIRMED_SPENDS_DIR_NAME); - let spend_hex_name = spend_addr.to_hex(); - let spend_file_path = spends_dir.join(spend_hex_name); - debug!("Try to getting a confirmed_spend instance from: {spend_file_path:?}"); - if !spend_file_path.exists() { - return Ok(None); - } - - let file = fs::File::open(&spend_file_path)?; - let confirmed_spend = rmp_serde::from_read(&file)?; - - Ok(Some(confirmed_spend)) -} - -/// Returns whether a spend is put as `confirmed`. -/// -/// Note: due to the disk operations' async behaviour. -/// reading a `exist` spend file, could end with a deserialization error. -pub(super) fn has_confirmed_spend(wallet_dir: &Path, spend_addr: SpendAddress) -> bool { - let spends_dir = wallet_dir.join(CONFIRMED_SPENDS_DIR_NAME); - let spend_hex_name = spend_addr.to_hex(); - let spend_file_path = spends_dir.join(spend_hex_name); - debug!("Try to getting a confirmed_spend instance from: {spend_file_path:?}"); - spend_file_path.exists() -} - -/// Returns `Some(Vec)` or None if file doesn't exist. -pub(super) fn get_unconfirmed_spend_requests( - wallet_dir: &Path, -) -> Result>> { - let path = wallet_dir.join(UNCONFIRMED_TX_NAME); - if !path.is_file() { - return Ok(None); - } - - let file = fs::File::open(&path)?; - let unconfirmed_spend_requests = rmp_serde::from_read(&file)?; - - Ok(Some(unconfirmed_spend_requests)) -} - -/// Hex encode and write each `CashNote` to a separate file in respective -/// recipient public address dir in the created cash_notes dir. Each file is named after the cash_note id. -pub(super) fn store_created_cash_notes<'a, T>( - created_cash_notes: T, - wallet_dir: &Path, -) -> Result<()> -where - T: IntoIterator, -{ - // The create cash_notes dir within the wallet dir. - let created_cash_notes_path = wallet_dir.join(CASHNOTES_DIR_NAME); - fs::create_dir_all(&created_cash_notes_path)?; - - for cash_note in created_cash_notes { - let unique_pubkey_file_name = format!( - "{}.cash_note", - SpendAddress::from_unique_pubkey(&cash_note.unique_pubkey()).to_hex() - ); - - let cash_note_file_path = created_cash_notes_path.join(unique_pubkey_file_name); - debug!("Writing cash_note file to: {cash_note_file_path:?}"); - - let hex = cash_note - .to_hex() - .map_err(|_| Error::FailedToHexEncodeCashNote)?; - fs::write(cash_note_file_path, &hex)?; - } - Ok(()) -} - -/// Hex encode and remove each `CashNote` from a separate file in respective -pub(super) fn remove_cash_notes<'a, T>(cash_notes: T, wallet_dir: &Path) -> Result<()> -where - T: IntoIterator, -{ - // The create cash_notes dir within the wallet dir. - let created_cash_notes_path = wallet_dir.join(CASHNOTES_DIR_NAME); - for cash_note_key in cash_notes { - let unique_pubkey_name = *SpendAddress::from_unique_pubkey(cash_note_key).xorname(); - let unique_pubkey_file_name = format!("{}.cash_note", hex::encode(unique_pubkey_name)); - - let cash_note_file_path = created_cash_notes_path.join(unique_pubkey_file_name); - debug!("Removing cash_note file from: {:?}", cash_note_file_path); - - fs::remove_file(cash_note_file_path)?; - } - Ok(()) -} - -/// Loads all the cash_notes found in the cash_notes dir. -pub(super) fn load_cash_notes_from_disk(wallet_dir: &Path) -> Result> { - let cash_notes_path = match std::env::var("CASHNOTES_PATH") { - Ok(path) => PathBuf::from(path), - Err(_) => wallet_dir.join(CASHNOTES_DIR_NAME), - }; - - let mut deposits = vec![]; - for entry in walkdir::WalkDir::new(&cash_notes_path) - .into_iter() - .flatten() - { - if entry.file_type().is_file() { - let file_name = entry.file_name(); - println!("Reading deposited tokens from {file_name:?}."); - - let cash_note_data = fs::read_to_string(entry.path())?; - let cash_note = match CashNote::from_hex(cash_note_data.trim()) { - Ok(cash_note) => cash_note, - Err(_) => { - println!( - "This file does not appear to have valid hex-encoded CashNote data. \ - Skipping it." - ); - continue; - } - }; - - deposits.push(cash_note); - } - } - - if deposits.is_empty() { - println!("No deposits found at {}.", cash_notes_path.display()); - } - - Ok(deposits) -} - -/// Loads a specific cash_note from path -pub fn load_created_cash_note(unique_pubkey: &UniquePubkey, wallet_dir: &Path) -> Option { - trace!("Loading cash_note from file with pubkey: {unique_pubkey:?}"); - let created_cash_notes_path = wallet_dir.join(CASHNOTES_DIR_NAME); - let unique_pubkey_name = *SpendAddress::from_unique_pubkey(unique_pubkey).xorname(); - let unique_pubkey_file_name = format!("{}.cash_note", hex::encode(unique_pubkey_name)); - // Construct the path to the cash_note file - let cash_note_file_path = created_cash_notes_path.join(unique_pubkey_file_name); - - // Read the cash_note data from the file - match fs::read_to_string(cash_note_file_path.clone()) { - Ok(cash_note_data) => { - // Convert the cash_note data from hex to CashNote - match CashNote::from_hex(cash_note_data.trim()) { - Ok(cash_note) => Some(cash_note), - Err(error) => { - warn!("Failed to convert cash_note data from hex: {}", error); - None - } - } - } - Err(error) => { - warn!( - "Failed to read cash_note file {:?}: {}", - cash_note_file_path, error - ); - None - } - } -} diff --git a/sn_transfers/src/wallet/watch_only.rs b/sn_transfers/src/wallet/watch_only.rs deleted file mode 100644 index adcd01590c..0000000000 --- a/sn_transfers/src/wallet/watch_only.rs +++ /dev/null @@ -1,423 +0,0 @@ -// Copyright 2024 MaidSafe.net limited. -// -// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3. -// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed -// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. Please review the Licences for the specific language governing -// permissions and limitations relating to use of the SAFE Network Software. - -use super::{ - api::WalletApi, - error::{Error, Result}, - hot_wallet::WalletExclusiveAccess, - keys::{get_main_pubkey, store_new_pubkey}, - wallet_file::{ - load_cash_notes_from_disk, load_created_cash_note, store_created_cash_notes, store_wallet, - wallet_lockfile_name, - }, - KeyLessWallet, -}; -use crate::{ - wallet::data_payments::PaymentDetails, CashNote, DerivationIndex, MainPubkey, NanoTokens, - SpendReason, UniquePubkey, UnsignedTransaction, -}; -#[cfg(not(target_arch = "wasm32"))] -use fs2::FileExt; -use std::{ - collections::{BTreeMap, BTreeSet}, - fs::OpenOptions, - path::{Path, PathBuf}, -}; -use xor_name::XorName; - -#[derive(serde::Serialize, serde::Deserialize)] -/// This assumes the CashNotes are stored on disk -pub struct WatchOnlyWallet { - /// Main public key which owns the cash notes. - main_pubkey: MainPubkey, - /// The dir of the wallet file, main key, public address, and new cash_notes. - wallet_dir: PathBuf, - /// Wallet APIs - api: WalletApi, - /// The wallet containing all data, cash notes & transactions data that gets serialised and stored on disk. - keyless_wallet: KeyLessWallet, -} - -impl WatchOnlyWallet { - // Creates a new instance (only in memory) with provided info - pub fn new(main_pubkey: MainPubkey, wallet_dir: &Path, keyless_wallet: KeyLessWallet) -> Self { - Self { - main_pubkey, - api: WalletApi::new_from_wallet_dir(wallet_dir), - wallet_dir: wallet_dir.to_path_buf(), - keyless_wallet, - } - } - - /// Insert a payment and write it to the `payments` dir. - /// If a prior payment has been made to the same xorname, then the new payment is pushed to the end of the list. - pub fn insert_payment_transaction(&self, name: XorName, payment: PaymentDetails) -> Result<()> { - self.api.insert_payment_transaction(name, payment) - } - - pub fn remove_payment_transaction(&self, name: &XorName) { - self.api.remove_payment_transaction(name) - } - - /// Try to load any new cash_notes from the `cash_notes` dir in the wallet dir. - pub fn try_load_cash_notes(&mut self) -> Result<()> { - let cash_notes = load_cash_notes_from_disk(&self.wallet_dir)?; - let spent_unique_pubkeys: BTreeSet<_> = cash_notes - .iter() - .flat_map(|cn| cn.parent_spends.iter().map(|s| s.unique_pubkey())) - .collect(); - self.deposit(&cash_notes)?; - self.mark_notes_as_spent(spent_unique_pubkeys); - - let exclusive_access = self.lock()?; - self.store(exclusive_access)?; - - Ok(()) - } - - /// Loads a serialized wallet from a given path and main pub key. - pub fn load_from(wallet_dir: &Path, main_pubkey: MainPubkey) -> Result { - let main_pubkey = match get_main_pubkey(wallet_dir)? { - Some(pk) if pk != main_pubkey => { - return Err(Error::PubKeyMismatch(wallet_dir.to_path_buf())) - } - Some(pk) => pk, - None => { - warn!("No main pub key found when loading wallet from path, storing it now: {main_pubkey:?}"); - std::fs::create_dir_all(wallet_dir)?; - store_new_pubkey(wallet_dir, &main_pubkey)?; - main_pubkey - } - }; - Self::load_keyless_wallet(wallet_dir, main_pubkey) - } - - /// Loads a serialized wallet from a given path, no additional element will - /// be added to the provided path and strictly taken as the wallet files location. - pub fn load_from_path(wallet_dir: &Path) -> Result { - let main_pubkey = - get_main_pubkey(wallet_dir)?.ok_or(Error::PubkeyNotFound(wallet_dir.to_path_buf()))?; - Self::load_keyless_wallet(wallet_dir, main_pubkey) - } - - pub fn address(&self) -> MainPubkey { - self.main_pubkey - } - - pub fn balance(&self) -> NanoTokens { - self.keyless_wallet.balance() - } - - pub fn wallet_dir(&self) -> &Path { - &self.wallet_dir - } - - pub fn api(&self) -> &WalletApi { - &self.api - } - - /// Deposit the given cash_notes onto the wallet (without storing them to disk). - pub fn deposit<'a, T>(&mut self, received_cash_notes: T) -> Result<()> - where - T: IntoIterator, - { - for cash_note in received_cash_notes { - let id = cash_note.unique_pubkey(); - - if cash_note.derived_pubkey(&self.main_pubkey).is_err() { - debug!("skipping: cash_note is not our key"); - continue; - } - - let value = cash_note.value(); - self.keyless_wallet.available_cash_notes.insert(id, value); - } - - Ok(()) - } - - /// Store the given cash_notes to the `cash_notes` dir in the wallet dir. - /// Update and store the updated wallet to disk - /// This function locks the wallet to prevent concurrent processes from writing to it - pub fn deposit_and_store_to_disk(&mut self, received_cash_notes: &Vec) -> Result<()> { - if received_cash_notes.is_empty() { - return Ok(()); - } - - std::fs::create_dir_all(&self.wallet_dir)?; - - // lock and load from disk to make sure we're up to date and others can't modify the wallet concurrently - let exclusive_access = self.lock()?; - self.reload()?; - trace!("Wallet locked and loaded!"); - - for cash_note in received_cash_notes { - let id = cash_note.unique_pubkey(); - - if cash_note.derived_pubkey(&self.main_pubkey).is_err() { - debug!("skipping: cash_note is not our key"); - continue; - } - - let value = cash_note.value(); - self.keyless_wallet.available_cash_notes.insert(id, value); - - store_created_cash_notes([cash_note], &self.wallet_dir)?; - } - - self.store(exclusive_access) - } - - /// Reloads the wallet from disk. - /// FIXME: this will drop any data held in memory and completely replaced with what's read fom disk. - pub fn reload(&mut self) -> Result<()> { - *self = Self::load_from(&self.wallet_dir, self.main_pubkey)?; - Ok(()) - } - - /// Attempts to reload the wallet from disk. - pub fn reload_from_disk_or_recreate(&mut self) -> Result<()> { - std::fs::create_dir_all(&self.wallet_dir)?; - let _exclusive_access = self.lock()?; - self.reload()?; - Ok(()) - } - - /// Return UniquePubkeys of cash_notes we own that are not yet spent. - pub fn available_cash_notes(&self) -> &BTreeMap { - &self.keyless_wallet.available_cash_notes - } - - /// Remove referenced CashNotes from available_cash_notes - pub fn mark_notes_as_spent<'a, T>(&mut self, unique_pubkeys: T) - where - T: IntoIterator, - { - for k in unique_pubkeys { - self.keyless_wallet.available_cash_notes.remove(k); - } - } - - pub fn build_unsigned_transaction( - &mut self, - to: Vec<(NanoTokens, MainPubkey)>, - reason_hash: Option, - ) -> Result { - let mut rng = &mut rand::rngs::OsRng; - // create a unique key for each output - let to_unique_keys: Vec<_> = to - .into_iter() - .map(|(amount, address)| { - ( - amount, - address, - DerivationIndex::random(&mut rng), - false, // not a change output - ) - }) - .collect(); - - trace!("Trying to lock wallet to get available cash_notes..."); - // lock and load from disk to make sure we're up to date and others can't modify the wallet concurrently - let exclusive_access = self.lock()?; - self.reload()?; - trace!("Wallet locked and loaded!"); - - // get the available cash_notes - let mut available_cash_notes = vec![]; - let wallet_dir = self.wallet_dir.to_path_buf(); - for (id, _token) in self.available_cash_notes().iter() { - if let Some(cash_note) = load_created_cash_note(id, &wallet_dir) { - available_cash_notes.push(cash_note.clone()); - } else { - warn!("Skipping CashNote {:?} because we don't have it", id); - } - } - debug!( - "Available CashNotes for local send: {:#?}", - available_cash_notes - ); - - let reason_hash = reason_hash.unwrap_or_default(); - - let unsigned_transaction = UnsignedTransaction::new( - available_cash_notes, - to_unique_keys, - self.address(), - reason_hash, - )?; - - info!( - "Spending keys: {:?}", - unsigned_transaction.spent_unique_keys() - ); - unsigned_transaction - .spent_unique_keys() - .iter() - .for_each(|(k, _amount)| { - self.mark_notes_as_spent(vec![k]); - }); - - trace!("Releasing wallet lock"); // by dropping exclusive_access - std::mem::drop(exclusive_access); - - Ok(unsigned_transaction) - } - - // Helpers - - // Read the KeyLessWallet from disk, or build an empty one, and return WatchOnlyWallet - fn load_keyless_wallet(wallet_dir: &Path, main_pubkey: MainPubkey) -> Result { - let keyless_wallet = match KeyLessWallet::load_from(wallet_dir)? { - Some(keyless_wallet) => { - debug!( - "Loaded wallet from {wallet_dir:#?} with balance {:?}", - keyless_wallet.balance() - ); - keyless_wallet - } - None => { - let keyless_wallet = KeyLessWallet::default(); - store_wallet(wallet_dir, &keyless_wallet)?; - keyless_wallet - } - }; - - Ok(Self { - main_pubkey, - api: WalletApi::new_from_wallet_dir(wallet_dir), - wallet_dir: wallet_dir.to_path_buf(), - keyless_wallet, - }) - } - - // Stores the wallet to disk. - // This requires having exclusive access to the wallet to prevent concurrent processes from writing to it - pub(super) fn store(&self, exclusive_access: WalletExclusiveAccess) -> Result<()> { - store_wallet(&self.wallet_dir, &self.keyless_wallet)?; - trace!("Releasing wallet lock"); - std::mem::drop(exclusive_access); - Ok(()) - } - - // Locks the wallet and returns exclusive access to the wallet - // This lock prevents any other process from locking the wallet dir, effectively acts as a mutex for the wallet - pub(super) fn lock(&self) -> Result { - let lock = wallet_lockfile_name(&self.wallet_dir); - let file = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(lock)?; - - #[cfg(not(target_arch = "wasm32"))] - file.lock_exclusive()?; - Ok(file) - } -} - -#[cfg(test)] -mod tests { - use super::WatchOnlyWallet; - use crate::{ - genesis::{create_first_cash_note_from_key, GENESIS_CASHNOTE_AMOUNT}, - wallet::KeyLessWallet, - MainSecretKey, NanoTokens, - }; - use assert_fs::TempDir; - use eyre::Result; - - #[test] - fn watchonly_wallet_basics() -> Result<()> { - let main_sk = MainSecretKey::random(); - let main_pubkey = main_sk.main_pubkey(); - let wallet_dir = TempDir::new()?; - let wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default()); - - assert_eq!(wallet_dir.path(), wallet.wallet_dir()); - assert_eq!(main_pubkey, wallet.address()); - assert_eq!(NanoTokens::zero(), wallet.balance()); - assert!(wallet.available_cash_notes().is_empty()); - - Ok(()) - } - - #[tokio::test] - async fn watchonly_wallet_to_and_from_file() -> Result<()> { - let main_sk = MainSecretKey::random(); - let main_pubkey = main_sk.main_pubkey(); - let cash_note = create_first_cash_note_from_key(&main_sk)?; - let wallet_dir = TempDir::new()?; - - let mut wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default()); - wallet.deposit_and_store_to_disk(&vec![cash_note])?; - - let deserialised = WatchOnlyWallet::load_from(&wallet_dir, main_pubkey)?; - - assert_eq!(deserialised.wallet_dir(), wallet.wallet_dir()); - assert_eq!(deserialised.address(), wallet.address()); - - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - assert_eq!(GENESIS_CASHNOTE_AMOUNT, deserialised.balance().as_nano()); - - assert_eq!(1, wallet.available_cash_notes().len()); - assert_eq!(1, deserialised.available_cash_notes().len()); - assert_eq!( - deserialised.available_cash_notes(), - wallet.available_cash_notes() - ); - - Ok(()) - } - - #[tokio::test] - async fn watchonly_wallet_deposit_cash_notes() -> Result<()> { - let main_sk = MainSecretKey::random(); - let main_pubkey = main_sk.main_pubkey(); - let wallet_dir = TempDir::new()?; - let mut wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default()); - - // depositing owned cash note shall be deposited and increase the balance - let owned_cash_note = create_first_cash_note_from_key(&main_sk)?; - wallet.deposit(&vec![owned_cash_note.clone()])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - - // depositing non-owned cash note shall be dropped and not change the balance - let non_owned_cash_note = create_first_cash_note_from_key(&MainSecretKey::random())?; - wallet.deposit(&vec![non_owned_cash_note])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - - // depositing is idempotent - wallet.deposit(&vec![owned_cash_note])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - - Ok(()) - } - - #[tokio::test] - async fn watchonly_wallet_reload() -> Result<()> { - let main_sk = MainSecretKey::random(); - let main_pubkey = main_sk.main_pubkey(); - let wallet_dir = TempDir::new()?; - let mut wallet = WatchOnlyWallet::new(main_pubkey, &wallet_dir, KeyLessWallet::default()); - - let cash_note = create_first_cash_note_from_key(&main_sk)?; - wallet.deposit(&vec![cash_note.clone()])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - - wallet.reload()?; - assert_eq!(NanoTokens::zero(), wallet.balance()); - - wallet.deposit_and_store_to_disk(&vec![cash_note])?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - wallet.reload()?; - assert_eq!(GENESIS_CASHNOTE_AMOUNT, wallet.balance().as_nano()); - - Ok(()) - } -} From ebb870203b99c7cd43327f2c5d4f6ba37cb2da0c Mon Sep 17 00:00:00 2001 From: grumbach Date: Fri, 29 Nov 2024 17:46:23 +0900 Subject: [PATCH 2/2] chore: cleanups --- autonomi/examples/metamask/index.js | 6 ++-- autonomi/src/client/wasm.rs | 14 ++++---- autonomi/tests/external_signer.rs | 19 +++++----- evmlib/artifacts/AutonomiNetworkToken.json | 12 +++---- evmlib/src/contract/network_token.rs | 30 +++++++--------- evmlib/src/external_signer.rs | 16 ++++----- evmlib/src/wallet.rs | 40 ++++++++++------------ evmlib/tests/network_token.rs | 8 ++--- node-launchpad/.config/config.json5 | 6 ++-- node-launchpad/src/action.rs | 2 +- node-launchpad/src/app.rs | 12 +++---- node-launchpad/src/tui.rs | 2 +- sn_networking/src/error.rs | 12 +++---- sn_networking/src/lib.rs | 20 ++++++++--- sn_node/README.md | 2 +- 15 files changed, 100 insertions(+), 101 deletions(-) diff --git a/autonomi/examples/metamask/index.js b/autonomi/examples/metamask/index.js index 235fa9f8c6..66bf524037 100644 --- a/autonomi/examples/metamask/index.js +++ b/autonomi/examples/metamask/index.js @@ -189,16 +189,16 @@ const executeQuotePayments = async (sender, quotes, quotePayments) => { quotePayments ); - // Form approve to transaction tokens calldata + // Form approve to spend tokens calldata const approveCalldata = autonomi.getApproveToSpendTokensCalldata( evmNetwork, - payForQuotesCalldata.approve_transactioner, + payForQuotesCalldata.approve_spender, payForQuotesCalldata.approve_amount ); console.log("Sending approve transaction.."); - // Approve to transaction tokens + // Approve to spend tokens let hash = await sendTransaction({ from: sender, to: approveCalldata[1], diff --git a/autonomi/src/client/wasm.rs b/autonomi/src/client/wasm.rs index cc0e03155a..6c3a151135 100644 --- a/autonomi/src/client/wasm.rs +++ b/autonomi/src/client/wasm.rs @@ -637,9 +637,7 @@ mod external_signer { use crate::client::external_signer::encrypt_data; use crate::client::payment::Receipt; use crate::receipt_from_quotes_and_payments; - use sn_evm::external_signer::{ - approve_to_transaction_tokens_calldata, pay_for_quotes_calldata, - }; + use sn_evm::external_signer::{approve_to_spend_tokens_calldata, pay_for_quotes_calldata}; use sn_evm::EvmNetwork; use sn_evm::QuotePayment; use sn_evm::{Amount, PaymentQuote}; @@ -742,17 +740,17 @@ mod external_signer { Ok(js_value) } - /// Form approve to transaction tokens calldata. + /// Form approve to spend tokens calldata. #[wasm_bindgen(js_name = getApproveToSpendTokensCalldata)] - pub fn get_approve_to_transaction_tokens_calldata( + pub fn get_approve_to_spend_tokens_calldata( network: JsValue, - transactioner: JsValue, + spender: JsValue, amount: JsValue, ) -> Result { let network: EvmNetwork = serde_wasm_bindgen::from_value(network)?; - let transactioner: EvmAddress = serde_wasm_bindgen::from_value(transactioner)?; + let spender: EvmAddress = serde_wasm_bindgen::from_value(spender)?; let amount: Amount = serde_wasm_bindgen::from_value(amount)?; - let calldata = approve_to_transaction_tokens_calldata(&network, transactioner, amount); + let calldata = approve_to_spend_tokens_calldata(&network, spender, amount); let js_value = serde_wasm_bindgen::to_value(&calldata)?; Ok(js_value) } diff --git a/autonomi/tests/external_signer.rs b/autonomi/tests/external_signer.rs index 4430ef519a..89c9cd4d48 100644 --- a/autonomi/tests/external_signer.rs +++ b/autonomi/tests/external_signer.rs @@ -50,21 +50,20 @@ async fn pay_for_content_addresses( // Init an external wallet provider. In the webapp, this would be MetaMask for example let provider = wallet.to_provider(); - // Form approve to transaction tokens transaction data - let approve_calldata = - autonomi::client::external_signer::approve_to_transaction_tokens_calldata( - wallet.network(), - pay_for_quotes_calldata.approve_transactioner, - pay_for_quotes_calldata.approve_amount, - ); - - // Prepare approve to transaction tokens transaction + // Form approve to spend tokens transaction data + let approve_calldata = autonomi::client::external_signer::approve_to_spend_tokens_calldata( + wallet.network(), + pay_for_quotes_calldata.approve_spender, + pay_for_quotes_calldata.approve_amount, + ); + + // Prepare approve to spend tokens transaction let transaction_request = provider .transaction_request() .with_to(approve_calldata.1) .with_input(approve_calldata.0); - // Send approve to transaction tokens transaction + // Send approve to spend tokens transaction let _tx_hash = provider .send_transaction(transaction_request) .await? diff --git a/evmlib/artifacts/AutonomiNetworkToken.json b/evmlib/artifacts/AutonomiNetworkToken.json index 0eac667117..841ed5d678 100644 --- a/evmlib/artifacts/AutonomiNetworkToken.json +++ b/evmlib/artifacts/AutonomiNetworkToken.json @@ -60,7 +60,7 @@ "inputs": [ { "internalType": "address", - "name": "transactioner", + "name": "spender", "type": "address" }, { @@ -135,7 +135,7 @@ "inputs": [ { "internalType": "address", - "name": "transactioner", + "name": "spender", "type": "address" } ], @@ -261,7 +261,7 @@ { "indexed": true, "internalType": "address", - "name": "transactioner", + "name": "spender", "type": "address" }, { @@ -390,7 +390,7 @@ }, { "internalType": "address", - "name": "transactioner", + "name": "spender", "type": "address" } ], @@ -409,7 +409,7 @@ "inputs": [ { "internalType": "address", - "name": "transactioner", + "name": "spender", "type": "address" }, { @@ -776,7 +776,7 @@ }, { "internalType": "address", - "name": "transactioner", + "name": "spender", "type": "address" }, { diff --git a/evmlib/src/contract/network_token.rs b/evmlib/src/contract/network_token.rs index 0ccf9c84d6..10903c9fd2 100644 --- a/evmlib/src/contract/network_token.rs +++ b/evmlib/src/contract/network_token.rs @@ -76,23 +76,23 @@ where } /// See how many tokens are approved to be spent. - pub async fn allowance(&self, owner: Address, transactioner: Address) -> Result { - debug!("Getting allowance of owner: {owner} for transactioner: {transactioner}",); + pub async fn allowance(&self, owner: Address, spender: Address) -> Result { + debug!("Getting allowance of owner: {owner} for spender: {spender}",); let balance = self .contract - .allowance(owner, transactioner) + .allowance(owner, spender) .call() .await .inspect_err(|err| error!("Error getting allowance: {err:?}"))? ._0; - debug!("Allowance of owner: {owner} for transactioner: {transactioner} is: {balance}"); + debug!("Allowance of owner: {owner} for spender: {spender} is: {balance}"); Ok(balance) } - /// Approve transactioner to transaction a raw amount of tokens. - pub async fn approve(&self, transactioner: Address, value: U256) -> Result { - debug!("Approving transactioner to transaction raw amt of tokens: {value}"); - let (calldata, to) = self.approve_calldata(transactioner, value); + /// Approve spender to spend a raw amount of tokens. + pub async fn approve(&self, spender: Address, value: U256) -> Result { + debug!("Approving spender to spend raw amt of tokens: {value}"); + let (calldata, to) = self.approve_calldata(spender, value); let transaction_request = self .contract @@ -108,13 +108,13 @@ where .await .inspect_err(|err| { error!( - "Error approving transactioner {transactioner:?} to transaction raw amt of tokens {value}: {err:?}" + "Error approving spender {spender:?} to spend raw amt of tokens {value}: {err:?}" ) })?; let pending_tx_hash = *pending_tx_builder.tx_hash(); - debug!("The approval from sender {transactioner:?} is pending with tx_hash: {pending_tx_hash:?}",); + debug!("The approval from sender {spender:?} is pending with tx_hash: {pending_tx_hash:?}",); let tx_hash = pending_tx_builder.watch().await.inspect_err(|err| { error!("Error watching approve tx with hash {pending_tx_hash:?}: {err:?}") @@ -125,14 +125,10 @@ where Ok(tx_hash) } - /// Approve transactioner to transaction a raw amount of tokens. + /// Approve spender to spend a raw amount of tokens. /// Returns the transaction calldata. - pub fn approve_calldata(&self, transactioner: Address, value: U256) -> (Calldata, Address) { - let calldata = self - .contract - .approve(transactioner, value) - .calldata() - .to_owned(); + pub fn approve_calldata(&self, spender: Address, value: U256) -> (Calldata, Address) { + let calldata = self.contract.approve(spender, value).calldata().to_owned(); (calldata, *self.contract.address()) } diff --git a/evmlib/src/external_signer.rs b/evmlib/src/external_signer.rs index 545c26c1df..20c3aa95df 100644 --- a/evmlib/src/external_signer.rs +++ b/evmlib/src/external_signer.rs @@ -23,17 +23,17 @@ pub enum Error { DataPaymentsContract(#[from] data_payments::error::Error), } -/// Approve an address / smart contract to transaction this wallet's payment tokens. +/// Approve an address / smart contract to spend this wallet's payment tokens. /// /// Returns the transaction calldata (input, to). -pub fn approve_to_transaction_tokens_calldata( +pub fn approve_to_spend_tokens_calldata( network: &Network, - transactioner: Address, + spender: Address, value: U256, ) -> (Calldata, Address) { let provider = http_provider(network.rpc_url().clone()); let network_token = NetworkToken::new(*network.payment_token_address(), provider); - network_token.approve_calldata(transactioner, value) + network_token.approve_calldata(spender, value) } /// Transfer payment tokens from the supplied wallet to an address. @@ -53,14 +53,14 @@ pub fn transfer_tokens_calldata( pub struct PayForQuotesCalldataReturnType { pub batched_calldata_map: HashMap>, pub to: Address, - pub approve_transactioner: Address, + pub approve_spender: Address, pub approve_amount: Amount, } /// Use this wallet to pay for chunks in batched transfer transactions. /// If the amount of transfers is more than one transaction can contain, the transfers will be split up over multiple transactions. /// -/// Returns PayForQuotesCalldataReturnType, containing calldata of the transaction batches along with the approval details for the transactioner. +/// Returns PayForQuotesCalldataReturnType, containing calldata of the transaction batches along with the approval details for the spender. pub fn pay_for_quotes_calldata>( network: &Network, payments: T, @@ -69,7 +69,7 @@ pub fn pay_for_quotes_calldata>( let total_amount = payments.iter().map(|(_, _, amount)| amount).sum(); - let approve_transactioner = *network.data_payments_address(); + let approve_spender = *network.data_payments_address(); let approve_amount = total_amount; let provider = http_provider(network.rpc_url().clone()); @@ -90,7 +90,7 @@ pub fn pay_for_quotes_calldata>( Ok(PayForQuotesCalldataReturnType { batched_calldata_map: calldata_map, to: *data_payments.contract.address(), - approve_transactioner, + approve_spender, approve_amount, }) } diff --git a/evmlib/src/wallet.rs b/evmlib/src/wallet.rs index 04365ce593..643d14bdf9 100644 --- a/evmlib/src/wallet.rs +++ b/evmlib/src/wallet.rs @@ -105,22 +105,18 @@ impl Wallet { transfer_gas_tokens(self.wallet.clone(), &self.network, to, amount).await } - /// See how many tokens of the owner may be spent by the transactioner. - pub async fn token_allowance( - &self, - transactioner: Address, - ) -> Result { - token_allowance(&self.network, self.address(), transactioner).await + /// See how many tokens of the owner may be spent by the spender. + pub async fn token_allowance(&self, spender: Address) -> Result { + token_allowance(&self.network, self.address(), spender).await } - /// Approve an address / smart contract to transaction this wallet's payment tokens. - pub async fn approve_to_transaction_tokens( + /// Approve an address / smart contract to spend this wallet's payment tokens. + pub async fn approve_to_spend_tokens( &self, - transactioner: Address, + spender: Address, amount: U256, ) -> Result { - approve_to_transaction_tokens(self.wallet.clone(), &self.network, transactioner, amount) - .await + approve_to_spend_tokens(self.wallet.clone(), &self.network, spender, amount).await } /// Pays for a single quote. Returns transaction hash of the payment. @@ -227,29 +223,29 @@ pub async fn balance_of_gas_tokens( Ok(balance) } -/// See how many tokens of the owner may be spent by the transactioner. +/// See how many tokens of the owner may be spent by the spender. pub async fn token_allowance( network: &Network, owner: Address, - transactioner: Address, + spender: Address, ) -> Result { - debug!("Getting allowance for owner: {owner} and transactioner: {transactioner}",); + debug!("Getting allowance for owner: {owner} and spender: {spender}",); let provider = http_provider(network.rpc_url().clone()); let network_token = NetworkToken::new(*network.payment_token_address(), provider); - network_token.allowance(owner, transactioner).await + network_token.allowance(owner, spender).await } -/// Approve an address / smart contract to transaction this wallet's payment tokens. -pub async fn approve_to_transaction_tokens( +/// Approve an address / smart contract to spend this wallet's payment tokens. +pub async fn approve_to_spend_tokens( wallet: EthereumWallet, network: &Network, - transactioner: Address, + spender: Address, amount: U256, ) -> Result { - debug!("Approving address/smart contract with {amount} tokens at address: {transactioner}",); + debug!("Approving address/smart contract with {amount} tokens at address: {spender}",); let provider = http_provider_with_wallet(network.rpc_url().clone(), wallet); let network_token = NetworkToken::new(*network.payment_token_address(), provider); - network_token.approve(transactioner, amount).await + network_token.approve(spender, amount).await } /// Transfer payment tokens from the supplied wallet to an address. @@ -323,8 +319,8 @@ pub async fn pay_for_quotes>( // TODO: Get rid of approvals altogether, by using permits or whatever.. if allowance < total_amount_to_be_paid { - // Approve the contract to transaction all the client's tokens. - approve_to_transaction_tokens( + // Approve the contract to spend all the client's tokens. + approve_to_spend_tokens( wallet.clone(), network, *network.data_payments_address(), diff --git a/evmlib/tests/network_token.rs b/evmlib/tests/network_token.rs index 6a703168ec..77e2a1d723 100644 --- a/evmlib/tests/network_token.rs +++ b/evmlib/tests/network_token.rs @@ -71,11 +71,11 @@ async fn test_approve() { let account = wallet_address(network_token.contract.provider().wallet()); let transaction_value = U256::from(1); - let transactioner = PrivateKeySigner::random(); + let spender = PrivateKeySigner::random(); - // Approve for the transactioner to transaction a value from the funds of the owner (our default account). + // Approve for the spender to spend a value from the funds of the owner (our default account). let approval_result = network_token - .approve(transactioner.address(), transaction_value) + .approve(spender.address(), transaction_value) .await; assert!( @@ -86,7 +86,7 @@ async fn test_approve() { let allowance = network_token .contract - .allowance(account, transactioner.address()) + .allowance(account, spender.address()) .call() .await .unwrap() diff --git a/node-launchpad/.config/config.json5 b/node-launchpad/.config/config.json5 index dc208acbda..63786942ce 100644 --- a/node-launchpad/.config/config.json5 +++ b/node-launchpad/.config/config.json5 @@ -31,7 +31,7 @@ "": "Quit", "": "Quit", "": "Quit", - "": "Sutransaction" // Sutransaction the application + "": "Suspend" // Suspend the application }, "Options": { "": {"SwitchScene":"Status"}, @@ -67,7 +67,7 @@ "": "Quit", "": "Quit", "": "Quit", - "": "Sutransaction" // Sutransaction the application + "": "Suspend" // Suspend the application }, "Help": { "": {"SwitchScene":"Status"}, @@ -82,7 +82,7 @@ "": "Quit", "": "Quit", "": "Quit", - "": "Sutransaction" // Sutransaction the application + "": "Suspend" // Suspend the application } } } diff --git a/node-launchpad/src/action.rs b/node-launchpad/src/action.rs index c5f8a0fb08..5f4669a4d7 100644 --- a/node-launchpad/src/action.rs +++ b/node-launchpad/src/action.rs @@ -32,7 +32,7 @@ pub enum Action { Tick, Render, Resize(u16, u16), - Sutransaction, + Suspend, Resume, Quit, Refresh, diff --git a/node-launchpad/src/app.rs b/node-launchpad/src/app.rs index 26ad1029e9..dac3f1e4a3 100644 --- a/node-launchpad/src/app.rs +++ b/node-launchpad/src/app.rs @@ -42,7 +42,7 @@ pub struct App { pub frame_rate: f64, pub components: Vec>, pub should_quit: bool, - pub should_sutransaction: bool, + pub should_suspend: bool, pub input_mode: InputMode, pub scene: Scene, pub last_tick_key_events: Vec, @@ -150,7 +150,7 @@ impl App { Box::new(upgrade_nodes), ], should_quit: false, - should_sutransaction: false, + should_suspend: false, input_mode: InputMode::Navigation, scene: Scene::Status, last_tick_key_events: Vec::new(), @@ -221,8 +221,8 @@ impl App { self.last_tick_key_events.drain(..); } Action::Quit => self.should_quit = true, - Action::Sutransaction => self.should_sutransaction = true, - Action::Resume => self.should_sutransaction = false, + Action::Suspend => self.should_suspend = true, + Action::Resume => self.should_suspend = false, Action::Resize(w, h) => { tui.resize(Rect::new(0, 0, w, h))?; tui.draw(|f| { @@ -296,8 +296,8 @@ impl App { }; } } - if self.should_sutransaction { - tui.sutransaction()?; + if self.should_suspend { + tui.suspend()?; action_tx.send(Action::Resume)?; tui = tui::Tui::new()? .tick_rate(self.tick_rate) diff --git a/node-launchpad/src/tui.rs b/node-launchpad/src/tui.rs index 41a25b31a6..32fe8bfc42 100644 --- a/node-launchpad/src/tui.rs +++ b/node-launchpad/src/tui.rs @@ -219,7 +219,7 @@ impl Tui { self.cancellation_token.cancel(); } - pub fn sutransaction(&mut self) -> Result<()> { + pub fn suspend(&mut self) -> Result<()> { self.exit()?; #[cfg(not(windows))] signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP)?; diff --git a/sn_networking/src/error.rs b/sn_networking/src/error.rs index fb6cf07853..6b8e1258e5 100644 --- a/sn_networking/src/error.rs +++ b/sn_networking/src/error.rs @@ -12,7 +12,7 @@ use libp2p::{ swarm::DialError, PeerId, TransportError, }; -use sn_protocol::storage::{Transaction, TransactionAddress}; +use sn_protocol::storage::TransactionAddress; use sn_protocol::{messages::Response, storage::RecordKind, NetworkAddress, PrettyPrintRecordKey}; use std::{ collections::{HashMap, HashSet}, @@ -45,7 +45,7 @@ pub enum GetRecordError { RecordNotFound, // Avoid logging the whole `Record` content by accident. /// The split record error will be handled at the network layer. - /// For transactions, it accumulates the transactions and returns a double transaction error if more than one. + /// For transactions, it accumulates the transactions /// For registers, it merges the registers and returns the merged record. #[error("Split Record has {} different copies", result_map.len())] SplitRecord { @@ -133,11 +133,9 @@ pub enum NetworkError { #[error("Failed to verify the ChunkProof with the provided quorum")] FailedToVerifyChunkProof(NetworkAddress), - // ---------- Spend Errors - #[error("Spend not found: {0:?}")] - NoSpendFoundInsideRecord(TransactionAddress), - #[error("Double transaction(s) attempt was detected. The signed transactions are: {0:?}")] - DoubleSpendAttempt(Vec), + // ---------- Transaction Errors + #[error("Transaction not found: {0:?}")] + NoTransactionFoundInsideRecord(TransactionAddress), // ---------- Store Error #[error("No Store Cost Responses")] diff --git a/sn_networking/src/lib.rs b/sn_networking/src/lib.rs index 083f66a4fb..5f27a9085e 100644 --- a/sn_networking/src/lib.rs +++ b/sn_networking/src/lib.rs @@ -596,7 +596,7 @@ impl Network { } /// Handle the split record error. - /// Spend: Accumulate transactions and return error if more than one. + /// Transaction: Accumulate transactions. /// Register: Merge registers and return the merged record. fn handle_split_record_error( result_map: &HashMap)>, @@ -700,14 +700,26 @@ impl Network { } } - // Allow for early bail if we've already seen a split SpendAttempt + // Return the accumulated transactions as a single record if accumulated_transactions.len() > 1 { info!("For record {pretty_key:?} task found split record for a transaction, accumulated and sending them as a single record"); let accumulated_transactions = accumulated_transactions .into_iter() .collect::>(); - - return Err(NetworkError::DoubleSpendAttempt(accumulated_transactions)); + let record = Record { + key: key.clone(), + value: try_serialize_record(&accumulated_transactions, RecordKind::Transaction) + .map_err(|err| { + error!( + "Error while serializing the accumulated transactions for {pretty_key:?}: {err:?}" + ); + NetworkError::from(err) + })? + .to_vec(), + publisher: None, + expires: None, + }; + return Ok(Some(record)); } else if !collected_registers.is_empty() { info!("For record {pretty_key:?} task found multiple registers, merging them."); let signed_register = collected_registers.iter().fold(collected_registers[0].clone(), |mut acc, x| { diff --git a/sn_node/README.md b/sn_node/README.md index 414a94c38f..99166551b3 100644 --- a/sn_node/README.md +++ b/sn_node/README.md @@ -120,7 +120,7 @@ default_dir = SafeNode.get_default_root_dir(peer_id) - `get_validation.rs`: Validation for GET requests - `put_validation.rs`: Validation for PUT requests - `replication.rs`: Data replication logic - - `transactions.rs`: Logic related to transactioning tokens or resources + - `transactions.rs`: Logic related to spending tokens or resources - `tests/`: Test files - `common/mod.rs`: Common utilities for tests - `data_with_churn.rs`: Tests related to data with churn