From 89f7a4b3d6329493cd2b4bc5f346a819c99d3e7b Mon Sep 17 00:00:00 2001 From: Adam Kelly <338792+aqk@users.noreply.github.com> Date: Tue, 29 Jun 2021 14:21:25 -0700 Subject: [PATCH] Pools mainnet (#7047) * added clarifying comments * WIP test * added WIP test * Refine genesis challenge. Remove unnecessary pool_puzzle function * Sign spend. Remove create_member_spend. Rename state transition function to create_travel_spend * Rename create_member_spend to create_travel_spend * Add singleton id logging * Enhance logging for debugging * renaming * rephrase inside the puzzle * fixed signing and added some support functions * Fix issue with announcement * Progress spending the singleton * Fix arguments to pool_state_to_inner_puzzle call * Fix arguments to pool_state_to_inner_puzzle * Improve error message when wallet is not running * Remove misleading message about missing wallet process, when problem is the farmer by making poolnft command error out earlier * Fix parent coin info bug * Multiple state transitions in one block * Lint * Remove assert * Fix incorrect p2_singleton_ph calculation (thanks nil00) * Update waiting room puzzle to accept genesis_challenge * Update calls to create_waiting * Go to waiting state from committed state * Augment debug_spend_bundle * fix 2 bugs in wallet * Fix lint * fix bad_agg_sig bug * Tests and lint * remove breakpoint * fix clvm tests for new hexs and hashs * Fixed a bug in the coin store that was probably from merging. (#6577) * Fixed a bug in the coin store that was probably from merging. * The exception doesn't need to be there * CI Lint fix * Added lifecycle tests for pooling drivers (#6610) * Ms.poolabsorb (#6615) * Support for absorbing rewards in pools (untested) * Style improvements * More work on absorb * Revert default root and remove log * Revert small plots * Use real sub slot iters * Update types * debug1 * Fix bugs * fix output of agg sig log messages * Make fewer calls to pw_status in test * remove old comment * logging and state management * logging * small bug fix & rename for accuracy * format * Fix types for uncurry function * lint * Update test to use exceptions * Change assumptions about self-pooling in lifecycle test * Install types for mypy * Revert "Install types for mypy" This reverts commit a82dcb712a6a97b8789b17c98cac8eafaffe90f5. * install types for mypy * install types for mypy * More keys * Remove flags requiring interactive prompts * Change initial spend to waiting room if self-pooling * lint * lint * linting * Refactor test * Use correct value in log message * update p2_singleton_or_delated_puzhash * initial version of pool wallet with p2_singleton_or_delay * run black formatting * fix rebase wonkiness * fix announcement code in p2_singleton_or_delayed * removed redundant defaulting standardised hexstr handling * lint fixes * Fixed pool lifecycle tests to current standards, but discovered tests are not validating signatures * Signatures validate on this test now although the test still does not check it. * Lint fix * Fixed plotnft show and linting errors * fixed failing farmer/harvester rpc test * lint fix * Commenting out some outdated tests * Updated test coverage * lint fix * Some minor P2singleton improvements (#6325) * Improve some debugging tools. * Tidy pool clvm. * Use `SINGLETON_STRUCT`. Remove unused `and` macro. * Use better name `SINGLETON_MOD_HASH`. * Finish lifecycle test suite. * Fixing for merge with chia-blockchain/pools_delayed_puzzle (#72) Co-authored-by: Matt Hauff * Default delay time was being set incorrectly * Extracted get_delayed_puz_info_from_launcher_spend to driver code * Ms.taproot plot2 (#6692) * Start work on adding taproot to new plots * Fix issue in block_tools * new test-cache * Lint * DID fixexs * Fix other tests * Python black * Fix full node store test * Ensure block index <= 128 bits. * fix test_pool_config test * fix comments in pool_config and in chialisp files * self_pool -> pool -> self_pool * Implement leaving pools * Fix conflicts with main via mini-rebase * Fixup rebase mistakes * Bring in Mariano's node discovery chagnes from pools.dev * Fix adapters - Thanks Richard * build tests * Add pools tests * Disable DID tests * farmer|protocol: Some renaming related to the pool protocol * farmer: Use `None` instead of `{}` and add local `pool_state` * protocol|farmer: Introduce and use `PoolErrorCode` * rename: `pool_payout_instructions` -> `payout_instructions` * refactor: `AuthenticationKeyInfo` -> `authentication_key` * refactor: Move `launcher_id` up * rename: Some variable name changes * rename: `points_balance` -> `points` * format: Squash aggregation into one line * farmer: Make `update_pool_state` public * farmer: Print traceback if `update_pool_state` fails * farmer: Periodically call `GET /pool_info`, add `_pool_get_pool_info` * farmer: Add `authentication_token_timeout` to `pool_state` Fetch it from `GET /pool_info` * protocol|farmer: Implement support for `GET|POST|PUT /farmer` * farmer: Make use of `GET|POST /farmer` - To make the farmer known by the pool - To update local balance/difficulty from the pool periodically * farmer|protocol: Adjust `POST /partial` to match the latest spec * farmer: Hash messages before signing * pools: Drop unused code * farmer: Fix aggregation of partial signatures * farmer: support self pooling, don't pool if url=="" * wallet: return uint64 for delay time, instead of bytes * pool: add error code for delay time too short * farmer: cleaner logging when no connection to pool * farmer: add harvester node id to pool protocol * Rename method (test fix) and lint fix * Change errors to warnings (pool communication) * Remove pool callbacks on a reorg * farmer: Continue earlier when no pool URL is provided * farmer: Print method in log * farmer: Handle exceptions for all pool endpoint calls * farmer|protocol: Keep track of failed requests to the pool * farmer: Fix typo which caused issue with pooling * wallet: simplify solution_to_extra_data * tests: Comment out DID tests which are not working yet * Remove DID Wallet test workflows * Return launcher_id when creating Pool Wallet * Name p2_singleton_puzzle_hash correctly * Improve 'test_singleton_lifecycle_fast.py'. * Make test more robust in the face of asynchronous adversity * Add commandline cmds for joining and leaving pools * Fix poolnft leave params * Remove redundant assignment brought in from main * Remove unneeded code * Style and parsimony * pool_puzzles: Check was wrong, and bad naming * format: Fix linting * format: Remove log and rename variable * pool-wallet: Fix self pooling with multiple pool wallets. Don't remove interested puzzle_hash * gui: Use pools branch * format: fix lint * Remove ununsed code, improve initial_pool_state_from_dict * farmer: Instantly update the config, when config file changes * format: Speed up loading of the authentication key * logging: less annoying logging * Test pool NFT creation directly to pool * Test switching pools without self-farming in between * lint * pooling: Use integer for protocol version (#6797) * pooling: Use integer for protocol version * pooling: Fix import * Update GUI commit * Ms.login2 (#6804) * pooling: Login WIP * pooling: add RPC for get_link * dont use timeout * pooling: rename to get_login_link * format: remove logging * Fix SES test * Required cli argument Co-authored-by: almog * farmer|protocols: Rename `current_difficulty` for `POST /partial` (#6807) * Fix to farm summary * Use target_puzzlehash param name in RPC call * Pool test coverage (#6782) * Improvement in test coverage and typing * Added an extra absorb to the pool lifecycle test (only works when merged with https://github.com/Chia-Network/chia-blockchain/pull/6733) * Added new drivers for the p2_singleton puzzles * Added new tests and test coverage for singletons * organize pools testing directory * black formatting * black formatting in venv * lint fix * Update CI tests * Fixing tests post rebase * lint fix * Minor readability fix Co-authored-by: matt * farmer: Drop `target_puzzle_hash` from `GET /farmer` and `GET /login` (#6816) * Allow creation of PlotNFTs in self-farming state * gui: Fix install with more RAM (#6821) * Allow implicit payout_address in self-pool state, improve error messages and param ergonomics * print units in non-standard wallets correctly * Fix farmer import * Make syncing message in CLI more intuitive like the GUI * Fix linting and show header hash instead of height * gui: Update to 725071236eff8c81d5b267dc8eb69d7e03f3df8c * Revert "Merge" This reverts commit 23a1e688c5fb4f72983fd896d4933336a365fe38, reversing changes made to a850246c6f4de4d2eb65c4ac1d6023431f3ba7e9. * Revert "Revert "Merge"" This reverts commit 680331859f5dc404cca9c2ff8f4a61df374db125. * Treat tx_record as Dict. Refactor tx submission * Also add passed-in coin spends when processing new blocks in reconsider_peak * Test utilities had moved * Fix import of moved block_tools * Potentially fix yaml * Previously didn't take the right part of this change * Add -y flag, improve commandline plotnft handling * Fix typo * Add -y flag to plotnft create * pool_wallet: Restore from DB properly * wallet: ignore bad pool configs * Reduce memory * pool_wallet: Add claim command * pool_wallet: Set transaction records to confirmed * wallet: Fix bug in transaction cache * Formatting and remove log * pool_wallet: CLI balance and improvements to plotnft_funcs.py * pool_wallet: Simplify, and fix issue with double submission * pool_wallet: Fix tests * pool_wallet: Don't allow switching before relative lock height * update gui * change to 3000 mem * Correct sense of -y flag for self-pooling * cli: Display payout instructions for pool * pool_wallet: Don't create massive transactions * cli: Improvements to plotnft * pool_wallet: Get correct pool state * pool_wallet: Use last transaction block to prevent condition failure * Add block height for current state * Add outstanding unconfirmed transactions to pw_status * Refine command line plotnft show pending transactions * Fix tests by using the correct output from pw_status * Try to fix windows build * Print expected leave height * label pool urls * pool_wallet: Don't include pool 1.75 rewards in total * wallet: Add RPC and CLI for deleting unconfirmed transactions for a wallet * pool_wallet: If farming to a pool, show 0 balance in wallet * pool_wallet: Show error message if invalid state, in CLI * pool_wallet: Don't allow switching if there are pending unconfirmed transactions * tests: Clean up pool test logging * tests: Fix lint * Changed the pool innerpuzzes (#6802) * overload solutions for pool_innerpuz parameters * Fix tests for reduced size puzzles * deleted messy deprecated test * Fix lint. * fix bug where spend types were the wrong way around * merge with richard's lint fix * fix wallet bug remove unnecessary signature add defun-inline for clarity * Swap to defun for absorb case Use cons box for member innerpuz solution * fix if statement for cons box p1 * remove unnecessary solution arg * quick innerpuz fix to make tests pass * Switch to key-value pairs Undo cons box solution in pool_member inner puzzle * fix singleton lifecycle test * added some comments to calrify the meaning on "ps" * lint fix * reduce label size, search for label when reconstructing from solution * no need to keep looping if `p` found * lint fix * Removed unecessary defun-inline and changed hyphens to underscores * Changed created_coin_value_or_0 to an inline function * Changed morph_condition to an inline function * Added a comment for odd_cons_m113 * Rename output_odd and odd_output_found * Add inline functions to document the lineage proof values * Stager two rewrite * Added an ASSER_MY_AMOUNT to p2_singleton_or_delayed * Extract truth functionality to singleton_truths.clib * Fix tree hashes * Changed truths to a struct rather than a list. * fix test_singletons update did_innerpuz * recompile did_innerpuz * fix a log error * Renamed variable and factored out code per @richardkiss * lint fix * switch launcher extra_data to key_value pairs * fix parsing of new format of extra_data in launcher solution * fix broken test for new launcher solution format * remove bare raise Co-authored-by: Richard Kiss Co-authored-by: Matt Hauff * Also add passed-in coin spends when processing new blocks in reconsider_peak (#6898) Co-authored-by: Adam Kelly * Moved debug_spend_bundle and added it to the SpendBundle object (#6840) * Moved debug_spend_bundle and added it to the SpendBundle object * Remove problematic typing * Add testnet config * wallet: Memory would get corrupted if there was an error (#6902) * wallet: Memory would get corrupted if there was an error * wallet: Use block_record * wallet: Add records in a full fork too * wallet: remove unnecessary arguments in CC and DID * add to cache, revert if transaction fails Co-authored-by: Yostra * Improve comment * pool_wallet: Fix driver bug * wallet: Fix memory corruption * gui: Update to latest * Increase memory size * tests: Add test for absorbing from pool * small fix in solution_to_extra_data * Fixed incorrect function name * pooling: Fix EOS handling in full node * [pools.testnet9]add post /partial and /farmer header (#6957) * Update farmer.py add post header * Update farmer_api.py add post header * Update chia/farmer/farmer.py Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> * Update chia/farmer/farmer_api.py Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> * Fix lint and cleanup farmer.py * farmer: Fix linting issues (#7010) * Handle the case of incorrectly formatted PoolState data returned from inner singleton * wallet: Resubmit transaction if not successful, rename to new_transaction_block_callback (#7008) * Fix lint in pool_puzzles * pooling: Fix owner private key lookup, and remove unnecessary argument * pooling: Clear target state on `delete_unconfirmed_transactions` * Lint * Fix non-deterministic test * Slight cleanup clvm driver code (#7028) * Return None when a deserialized CLVM structure does not fit the expected format of var-value pair for singleton data * lint Co-authored-by: Adam Kelly * Revert "Add testnet config" This reverts commit 98124427241b8a268fbab43ac116887c89e9974f. Co-authored-by: matt Co-authored-by: Adam Kelly Co-authored-by: Mariano Sorgente Co-authored-by: Matt Hauff Co-authored-by: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Co-authored-by: Adam Co-authored-by: Adam Kelly Co-authored-by: Richard Kiss Co-authored-by: xdustinface Co-authored-by: almog Co-authored-by: dustinface <35775977+xdustinface@users.noreply.github.com> Co-authored-by: Earle Lowe Co-authored-by: arvidn Co-authored-by: willi123yao Co-authored-by: arty Co-authored-by: William Blanke Co-authored-by: matt-o-how <48453825+matt-o-how@users.noreply.github.com> Co-authored-by: Chris Marslender Co-authored-by: Yostra Co-authored-by: DouCrazy <43004977+lpf763827726@users.noreply.github.com> --- .github/workflows/build-macos-installer.yml | 10 +- .../workflows/build-test-macos-blockchain.yml | 2 +- .github/workflows/build-test-macos-clvm.yml | 2 +- .../build-test-macos-core-consensus.yml | 2 +- .../build-test-macos-core-daemon.yml | 93 ++ ...ld-test-macos-core-full_node-full_sync.yml | 2 +- .../build-test-macos-core-full_node.yml | 4 +- .../build-test-macos-core-server.yml | 2 +- .../workflows/build-test-macos-core-ssl.yml | 2 +- .../workflows/build-test-macos-core-types.yml | 4 +- .../workflows/build-test-macos-core-util.yml | 4 +- .github/workflows/build-test-macos-core.yml | 2 +- .../workflows/build-test-macos-generator.yml | 4 +- .github/workflows/build-test-macos-pools.yml | 93 ++ .../workflows/build-test-macos-simulation.yml | 2 +- .../build-test-macos-wallet-cc_wallet.yml | 2 +- .../build-test-macos-wallet-rl_wallet.yml | 2 +- .../workflows/build-test-macos-wallet-rpc.yml | 2 +- .../build-test-macos-wallet-sync.yml | 2 +- .github/workflows/build-test-macos-wallet.yml | 4 +- .../build-test-ubuntu-blockchain.yml | 2 +- .github/workflows/build-test-ubuntu-clvm.yml | 2 +- .../build-test-ubuntu-core-consensus.yml | 2 +- .../build-test-ubuntu-core-daemon.yml | 105 +++ ...d-test-ubuntu-core-full_node-full_sync.yml | 2 +- .../build-test-ubuntu-core-full_node.yml | 4 +- .../build-test-ubuntu-core-server.yml | 2 +- .../workflows/build-test-ubuntu-core-ssl.yml | 2 +- .../build-test-ubuntu-core-types.yml | 4 +- .../workflows/build-test-ubuntu-core-util.yml | 4 +- .github/workflows/build-test-ubuntu-core.yml | 2 +- .../workflows/build-test-ubuntu-generator.yml | 4 +- .github/workflows/build-test-ubuntu-pools.yml | 105 +++ .../build-test-ubuntu-simulation.yml | 2 +- .../build-test-ubuntu-wallet-cc_wallet.yml | 2 +- .../build-test-ubuntu-wallet-rl_wallet.yml | 2 +- .../build-test-ubuntu-wallet-rpc.yml | 2 +- .../build-test-ubuntu-wallet-sync.yml | 2 +- .../workflows/build-test-ubuntu-wallet.yml | 4 +- .gitmodules | 2 +- build_scripts/build_windows.ps1 | 1 + chia-blockchain-gui | 2 +- chia/clvm/singleton.py | 5 + chia/cmds/chia.py | 2 + chia/cmds/farm_funcs.py | 43 +- chia/cmds/keys_funcs.py | 2 +- chia/cmds/plotnft.py | 142 +++ chia/cmds/plotnft_funcs.py | 327 +++++++ chia/cmds/show.py | 14 +- chia/cmds/wallet.py | 20 + chia/cmds/wallet_funcs.py | 54 +- chia/consensus/block_body_validation.py | 1 + chia/consensus/coinbase.py | 2 +- chia/consensus/constants.py | 1 + chia/consensus/default_constants.py | 1 + chia/farmer/farmer.py | 367 +++++++- chia/farmer/farmer_api.py | 261 +++++- chia/full_node/full_node.py | 2 +- chia/full_node/full_node_store.py | 15 + chia/full_node/generator.py | 2 +- chia/full_node/mempool_check_conditions.py | 6 + chia/full_node/mempool_manager.py | 5 + chia/harvester/harvester.py | 3 +- chia/harvester/harvester_api.py | 52 +- chia/plotting/create_plots.py | 6 +- chia/plotting/plot_tools.py | 5 +- chia/pools/__init__.py | 0 chia/pools/pool_config.py | 66 ++ chia/pools/pool_puzzles.py | 430 +++++++++ chia/pools/pool_wallet.py | 869 ++++++++++++++++++ chia/pools/pool_wallet_info.py | 115 +++ chia/protocols/harvester_protocol.py | 38 +- chia/protocols/pool_protocol.py | 197 +++- chia/protocols/protocol_message_types.py | 7 +- chia/rpc/farmer_rpc_api.py | 30 +- chia/rpc/farmer_rpc_client.py | 18 +- chia/rpc/full_node_rpc_api.py | 120 +++ chia/rpc/full_node_rpc_client.py | 42 +- chia/rpc/wallet_rpc_api.py | 179 +++- chia/rpc/wallet_rpc_client.py | 93 +- chia/server/node_discovery.py | 2 +- chia/server/rate_limits.py | 2 + chia/types/blockchain_format/coin.py | 2 +- chia/types/blockchain_format/program.py | 24 +- .../types/blockchain_format/proof_of_space.py | 22 +- chia/types/blockchain_format/vdf.py | 4 +- chia/types/spend_bundle.py | 4 + chia/util/config.py | 4 +- chia/wallet/cc_wallet/cc_wallet.py | 2 +- chia/wallet/derive_keys.py | 43 +- chia/wallet/did_wallet/did_info.py | 4 +- chia/wallet/did_wallet/did_wallet.py | 234 ++++- chia/wallet/did_wallet/did_wallet_puzzles.py | 22 +- .../ccparent.py => lineage_proof.py} | 2 +- chia/wallet/puzzles/curry-and-treehash.clinc | 68 ++ chia/wallet/puzzles/did_innerpuz.clvm | 121 +-- chia/wallet/puzzles/did_innerpuz.clvm.hex | 2 +- .../puzzles/did_innerpuz.clvm.hex.sha256tree | 2 +- chia/wallet/puzzles/p2_singleton.clvm | 38 + chia/wallet/puzzles/p2_singleton.clvm.hex | 1 + .../puzzles/p2_singleton.clvm.hex.sha256tree | 1 + .../p2_singleton_or_delayed_puzhash.clvm | 58 ++ .../p2_singleton_or_delayed_puzhash.clvm.hex | 1 + ...ton_or_delayed_puzhash.clvm.hex.sha256tree | 1 + chia/wallet/puzzles/pool_member_innerpuz.clvm | 70 ++ .../puzzles/pool_member_innerpuz.clvm.hex | 1 + .../pool_member_innerpuz.clvm.hex.sha256tree | 1 + .../puzzles/pool_waitingroom_innerpuz.clvm | 69 ++ .../pool_waitingroom_innerpuz.clvm.hex | 1 + ...l_waitingroom_innerpuz.clvm.hex.sha256tree | 1 + chia/wallet/puzzles/prefarm/spend_prefarm.py | 33 +- chia/wallet/puzzles/singleton_launcher.clvm | 16 + .../puzzles/singleton_launcher.clvm.hex | 1 + .../singleton_launcher.clvm.hex.sha256tree | 1 + chia/wallet/puzzles/singleton_top_layer.clvm | 234 +++-- .../puzzles/singleton_top_layer.clvm.hex | 2 +- .../singleton_top_layer.clvm.hex.sha256tree | 2 +- chia/wallet/puzzles/singleton_top_layer.py | 188 ++++ chia/wallet/puzzles/singleton_truths.clib | 21 + chia/wallet/puzzles/test_cc.py | 10 +- chia/wallet/rl_wallet/rl_wallet.py | 10 +- chia/wallet/sign_coin_solutions.py | 12 +- chia/wallet/trade_manager.py | 9 +- chia/wallet/transaction_record.py | 2 +- .../{cc_wallet => util}/debug_spend_bundle.py | 84 +- chia/wallet/util/trade_utils.py | 2 +- chia/wallet/util/wallet_types.py | 1 + chia/wallet/wallet.py | 27 +- chia/wallet/wallet_block_store.py | 43 +- chia/wallet/wallet_blockchain.py | 93 +- chia/wallet/wallet_coin_store.py | 8 +- chia/wallet/wallet_interested_store.py | 101 ++ chia/wallet/wallet_node.py | 109 ++- chia/wallet/wallet_pool_store.py | 117 +++ chia/wallet/wallet_puzzle_store.py | 14 +- chia/wallet/wallet_state_manager.py | 231 +++-- chia/wallet/wallet_transaction_store.py | 12 +- chia/wallet/wallet_user_store.py | 24 +- install-gui.sh | 2 + setup.py | 2 + tests/block_tools.py | 60 +- tests/clvm/coin_store.py | 95 +- tests/clvm/test_clvm_compilation.py | 5 + tests/clvm/test_singletons.py | 519 +++++++++++ .../full_node/full_sync/test_full_sync.py | 4 +- tests/core/full_node/test_block_store.py | 3 + tests/core/full_node/test_conditions.py | 16 +- tests/core/full_node/test_full_node.py | 8 +- tests/core/full_node/test_full_node_store.py | 42 +- tests/core/node_height.py | 7 + tests/core/test_farmer_harvester_rpc.py | 42 +- tests/core/test_full_node_rpc.py | 158 +++- tests/pools/__init__.py | 0 tests/pools/test_pool_config.py | 43 + tests/pools/test_pool_puzzles_lifecycle.py | 417 +++++++++ tests/pools/test_pool_rpc.py | 851 +++++++++++++++++ tests/pools/test_pool_wallet.py | 70 ++ tests/pools/test_wallet_pool_store.py | 135 +++ tests/runner-templates/build-test-macos | 2 +- tests/runner-templates/build-test-ubuntu | 4 +- .../checkout-test-plots.include.yml | 2 +- tests/testconfig.py | 2 +- tests/wallet/cc_wallet/test_cc_wallet.py | 10 +- tests/wallet/did_wallet/test_did.py | 48 +- tests/wallet/rpc/test_wallet_rpc.py | 79 +- tests/wallet/test_singleton.py | 110 ++- tests/wallet/test_singleton_lifecycle.py | 146 +++ tests/wallet/test_singleton_lifecycle_fast.py | 749 +++++++++++++++ tests/wallet/test_wallet_interested_store.py | 59 ++ 169 files changed, 9165 insertions(+), 780 deletions(-) create mode 100644 .github/workflows/build-test-macos-core-daemon.yml create mode 100644 .github/workflows/build-test-macos-pools.yml create mode 100644 .github/workflows/build-test-ubuntu-core-daemon.yml create mode 100644 .github/workflows/build-test-ubuntu-pools.yml create mode 100644 chia/clvm/singleton.py create mode 100644 chia/cmds/plotnft.py create mode 100644 chia/cmds/plotnft_funcs.py create mode 100644 chia/pools/__init__.py create mode 100644 chia/pools/pool_config.py create mode 100644 chia/pools/pool_puzzles.py create mode 100644 chia/pools/pool_wallet.py create mode 100644 chia/pools/pool_wallet_info.py rename chia/wallet/{cc_wallet/ccparent.py => lineage_proof.py} (91%) create mode 100644 chia/wallet/puzzles/curry-and-treehash.clinc create mode 100644 chia/wallet/puzzles/p2_singleton.clvm create mode 100644 chia/wallet/puzzles/p2_singleton.clvm.hex create mode 100644 chia/wallet/puzzles/p2_singleton.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm create mode 100644 chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex create mode 100644 chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/pool_member_innerpuz.clvm create mode 100644 chia/wallet/puzzles/pool_member_innerpuz.clvm.hex create mode 100644 chia/wallet/puzzles/pool_member_innerpuz.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm create mode 100644 chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex create mode 100644 chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/singleton_launcher.clvm create mode 100644 chia/wallet/puzzles/singleton_launcher.clvm.hex create mode 100644 chia/wallet/puzzles/singleton_launcher.clvm.hex.sha256tree create mode 100644 chia/wallet/puzzles/singleton_top_layer.py create mode 100644 chia/wallet/puzzles/singleton_truths.clib rename chia/wallet/{cc_wallet => util}/debug_spend_bundle.py (58%) create mode 100644 chia/wallet/wallet_interested_store.py create mode 100644 chia/wallet/wallet_pool_store.py create mode 100644 tests/clvm/test_singletons.py create mode 100644 tests/pools/__init__.py create mode 100644 tests/pools/test_pool_config.py create mode 100644 tests/pools/test_pool_puzzles_lifecycle.py create mode 100644 tests/pools/test_pool_rpc.py create mode 100644 tests/pools/test_pool_wallet.py create mode 100644 tests/pools/test_wallet_pool_store.py create mode 100644 tests/wallet/test_singleton_lifecycle.py create mode 100644 tests/wallet/test_singleton_lifecycle_fast.py create mode 100644 tests/wallet/test_wallet_interested_store.py diff --git a/.github/workflows/build-macos-installer.yml b/.github/workflows/build-macos-installer.yml index 54b65ab0f80d..6b2b2c809276 100644 --- a/.github/workflows/build-macos-installer.yml +++ b/.github/workflows/build-macos-installer.yml @@ -135,17 +135,17 @@ jobs: - name: Rename Artifact run: | - ls - mv ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg + ls + mv ${{ github.workspace }}/build_scripts/final_installer/Chia-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg - name: Create Checksums run: | - ls - shasum -a 256 ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg > ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 + ls + shasum -a 256 ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg > ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg.sha256 - name: Upload to s3 run: | - aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/builds/ + aws s3 cp ${{ github.workspace }}/build_scripts/final_installer/Chia-Catalina-${{ steps.version_number.outputs.CHIA_INSTALLER_VERSION }}.dmg s3://download-chia-net/builds/ - name: Install py3createtorrent if: startsWith(github.ref, 'refs/tags/') diff --git a/.github/workflows/build-test-macos-blockchain.yml b/.github/workflows/build-test-macos-blockchain.yml index b58ca5b89db4..1e04e06cc28c 100644 --- a/.github/workflows/build-test-macos-blockchain.yml +++ b/.github/workflows/build-test-macos-blockchain.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-clvm.yml b/.github/workflows/build-test-macos-clvm.yml index 7ccfe26c8629..628c19c4ad92 100644 --- a/.github/workflows/build-test-macos-clvm.yml +++ b/.github/workflows/build-test-macos-clvm.yml @@ -80,4 +80,4 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py -s -v --durations 0 -n auto + ./venv/bin/py.test tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py tests/clvm/test_singletons.py -s -v --durations 0 -n auto diff --git a/.github/workflows/build-test-macos-core-consensus.yml b/.github/workflows/build-test-macos-core-consensus.yml index 2eed69cc358f..ac7b5504b8bd 100644 --- a/.github/workflows/build-test-macos-core-consensus.yml +++ b/.github/workflows/build-test-macos-core-consensus.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-core-daemon.yml b/.github/workflows/build-test-macos-core-daemon.yml new file mode 100644 index 000000000000..b029335831da --- /dev/null +++ b/.github/workflows/build-test-macos-core-daemon.yml @@ -0,0 +1,93 @@ +name: MacOS core-daemon Tests + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + name: MacOS core-daemon Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.8, 3.9] + os: [macOS-latest] + + steps: + - name: Cancel previous runs on the same branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: styfle/cancel-workflow-action@0.9.0 + with: + access_token: ${{ github.token }} + + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + # Note that new runners may break this https://github.com/actions/cache/issues/292 + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v2 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.27.0' + fetch-depth: 1 + + - name: Link home directory + run: | + cd $HOME + ln -s $GITHUB_WORKSPACE/.chia + echo "$HOME/.chia" + ls -al $HOME/.chia + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + BUILD_VDF_CLIENT: "N" + run: | + brew install boost + sh install.sh + + - name: Install timelord + run: | + . ./activate + sh install-timelord.sh + ./vdf_bench square_asm 400000 + + - name: Install developer requirements + run: | + . ./activate + venv/bin/python -m pip install pytest pytest-asyncio pytest-xdist + + - name: Test core-daemon code with pytest + run: | + . ./activate + ./venv/bin/py.test tests/core/daemon/test_daemon.py -s -v --durations 0 diff --git a/.github/workflows/build-test-macos-core-full_node-full_sync.yml b/.github/workflows/build-test-macos-core-full_node-full_sync.yml index 8e6362edd0d4..57b71287ae55 100644 --- a/.github/workflows/build-test-macos-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-macos-core-full_node-full_sync.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-core-full_node.yml b/.github/workflows/build-test-macos-core-full_node.yml index 3fe2a1777904..49d5ca95761d 100644 --- a/.github/workflows/build-test-macos-core-full_node.yml +++ b/.github/workflows/build-test-macos-core-full_node.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -90,4 +90,4 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_address_manager.py tests/core/full_node/test_block_store.py tests/core/full_node/test_coin_store.py tests/core/full_node/test_full_node.py tests/core/full_node/test_full_node_store.py tests/core/full_node/test_initial_freeze.py tests/core/full_node/test_mempool.py tests/core/full_node/test_mempool_performance.py tests/core/full_node/test_node_load.py tests/core/full_node/test_sync_store.py tests/core/full_node/test_transactions.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/test_address_manager.py tests/core/full_node/test_block_store.py tests/core/full_node/test_coin_store.py tests/core/full_node/test_conditions.py tests/core/full_node/test_full_node.py tests/core/full_node/test_full_node_store.py tests/core/full_node/test_initial_freeze.py tests/core/full_node/test_mempool.py tests/core/full_node/test_mempool_performance.py tests/core/full_node/test_node_load.py tests/core/full_node/test_performance.py tests/core/full_node/test_sync_store.py tests/core/full_node/test_transactions.py -s -v --durations 0 diff --git a/.github/workflows/build-test-macos-core-server.yml b/.github/workflows/build-test-macos-core-server.yml index b854f51a1fc3..02724c49ae56 100644 --- a/.github/workflows/build-test-macos-core-server.yml +++ b/.github/workflows/build-test-macos-core-server.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-core-ssl.yml b/.github/workflows/build-test-macos-core-ssl.yml index 9d86c2954452..1acd02e9a7e0 100644 --- a/.github/workflows/build-test-macos-core-ssl.yml +++ b/.github/workflows/build-test-macos-core-ssl.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-core-types.yml b/.github/workflows/build-test-macos-core-types.yml index bb72dfdec343..9c5a0198dfbf 100644 --- a/.github/workflows/build-test-macos-core-types.yml +++ b/.github/workflows/build-test-macos-core-types.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -90,4 +90,4 @@ jobs: - name: Test core-types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/types/test_proof_of_space.py -s -v --durations 0 + ./venv/bin/py.test tests/core/types/test_coin.py tests/core/types/test_proof_of_space.py -s -v --durations 0 diff --git a/.github/workflows/build-test-macos-core-util.yml b/.github/workflows/build-test-macos-core-util.yml index 499e3424b493..d9d46cff1cce 100644 --- a/.github/workflows/build-test-macos-core-util.yml +++ b/.github/workflows/build-test-macos-core-util.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -90,4 +90,4 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_keychain.py tests/core/util/test_significant_bits.py tests/core/util/test_streamable.py tests/core/util/test_type_checking.py -s -v --durations 0 + ./venv/bin/py.test tests/core/util/test_keychain.py tests/core/util/test_lru_cache.py tests/core/util/test_significant_bits.py tests/core/util/test_streamable.py tests/core/util/test_type_checking.py -s -v --durations 0 diff --git a/.github/workflows/build-test-macos-core.yml b/.github/workflows/build-test-macos-core.yml index 04c7769b1955..49d6e20cd4cb 100644 --- a/.github/workflows/build-test-macos-core.yml +++ b/.github/workflows/build-test-macos-core.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-generator.yml b/.github/workflows/build-test-macos-generator.yml index 7602cd5bcdda..a0d26262448e 100644 --- a/.github/workflows/build-test-macos-generator.yml +++ b/.github/workflows/build-test-macos-generator.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -90,4 +90,4 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_compression.py tests/generator/test_generator_types.py tests/generator/test_scan.py -s -v --durations 0 + ./venv/bin/py.test tests/generator/test_compression.py tests/generator/test_generator_types.py tests/generator/test_rom.py tests/generator/test_scan.py -s -v --durations 0 diff --git a/.github/workflows/build-test-macos-pools.yml b/.github/workflows/build-test-macos-pools.yml new file mode 100644 index 000000000000..d59e1b6a770c --- /dev/null +++ b/.github/workflows/build-test-macos-pools.yml @@ -0,0 +1,93 @@ +name: MacOS pools Tests + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + name: MacOS pools Tests + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.8, 3.9] + os: [macOS-latest] + + steps: + - name: Cancel previous runs on the same branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: styfle/cancel-workflow-action@0.9.0 + with: + access_token: ${{ github.token }} + + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + # Note that new runners may break this https://github.com/actions/cache/issues/292 + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v2 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.27.0' + fetch-depth: 1 + + - name: Link home directory + run: | + cd $HOME + ln -s $GITHUB_WORKSPACE/.chia + echo "$HOME/.chia" + ls -al $HOME/.chia + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + BUILD_VDF_CLIENT: "N" + run: | + brew install boost + sh install.sh + + - name: Install timelord + run: | + . ./activate + sh install-timelord.sh + ./vdf_bench square_asm 400000 + + - name: Install developer requirements + run: | + . ./activate + venv/bin/python -m pip install pytest pytest-asyncio pytest-xdist + + - name: Test pools code with pytest + run: | + . ./activate + ./venv/bin/py.test tests/pools/test_pool_config.py tests/pools/test_pool_puzzles_lifecycle.py tests/pools/test_pool_rpc.py tests/pools/test_pool_wallet.py tests/pools/test_wallet_pool_store.py -s -v --durations 0 diff --git a/.github/workflows/build-test-macos-simulation.yml b/.github/workflows/build-test-macos-simulation.yml index 997cfd849b9c..b132fdf35f94 100644 --- a/.github/workflows/build-test-macos-simulation.yml +++ b/.github/workflows/build-test-macos-simulation.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-wallet-cc_wallet.yml b/.github/workflows/build-test-macos-wallet-cc_wallet.yml index 0c66105f242b..ea79d50f349d 100644 --- a/.github/workflows/build-test-macos-wallet-cc_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-cc_wallet.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-wallet-rl_wallet.yml b/.github/workflows/build-test-macos-wallet-rl_wallet.yml index c84b68c52fd6..848b03fad37b 100644 --- a/.github/workflows/build-test-macos-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-macos-wallet-rl_wallet.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-wallet-rpc.yml b/.github/workflows/build-test-macos-wallet-rpc.yml index 28598bec2183..a2a6b0613151 100644 --- a/.github/workflows/build-test-macos-wallet-rpc.yml +++ b/.github/workflows/build-test-macos-wallet-rpc.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-wallet-sync.yml b/.github/workflows/build-test-macos-wallet-sync.yml index e7503e6b9d9b..d244834c36d4 100644 --- a/.github/workflows/build-test-macos-wallet-sync.yml +++ b/.github/workflows/build-test-macos-wallet-sync.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-macos-wallet.yml b/.github/workflows/build-test-macos-wallet.yml index b3534291066f..3a6f76953d77 100644 --- a/.github/workflows/build-test-macos-wallet.yml +++ b/.github/workflows/build-test-macos-wallet.yml @@ -58,7 +58,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -90,4 +90,4 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_backup.py tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_store.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/test_backup.py tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_store.py -s -v --durations 0 diff --git a/.github/workflows/build-test-ubuntu-blockchain.yml b/.github/workflows/build-test-ubuntu-blockchain.yml index 2127b0e18c3b..fd7f138f1b37 100644 --- a/.github/workflows/build-test-ubuntu-blockchain.yml +++ b/.github/workflows/build-test-ubuntu-blockchain.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-clvm.yml b/.github/workflows/build-test-ubuntu-clvm.yml index c4c98d4111da..3ae72921d83c 100644 --- a/.github/workflows/build-test-ubuntu-clvm.yml +++ b/.github/workflows/build-test-ubuntu-clvm.yml @@ -92,4 +92,4 @@ jobs: - name: Test clvm code with pytest run: | . ./activate - ./venv/bin/py.test tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py -s -v --durations 0 -n auto + ./venv/bin/py.test tests/clvm/test_chialisp_deserialization.py tests/clvm/test_clvm_compilation.py tests/clvm/test_puzzles.py tests/clvm/test_serialized_program.py tests/clvm/test_singletons.py -s -v --durations 0 -n auto diff --git a/.github/workflows/build-test-ubuntu-core-consensus.yml b/.github/workflows/build-test-ubuntu-core-consensus.yml index 73c2e47df381..02748728d2cf 100644 --- a/.github/workflows/build-test-ubuntu-core-consensus.yml +++ b/.github/workflows/build-test-ubuntu-core-consensus.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-core-daemon.yml b/.github/workflows/build-test-ubuntu-core-daemon.yml new file mode 100644 index 000000000000..0a10704f547f --- /dev/null +++ b/.github/workflows/build-test-ubuntu-core-daemon.yml @@ -0,0 +1,105 @@ +name: Ubuntu core-daemon Test + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + name: Ubuntu core-daemon Test + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest] + + steps: + - name: Cancel previous runs on the same branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: styfle/cancel-workflow-action@0.9.0 + with: + access_token: ${{ github.token }} + + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache npm + uses: actions/cache@v2.1.6 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v2 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.27.0' + fetch-depth: 1 + + - name: Link home directory + run: | + cd $HOME + ln -s $GITHUB_WORKSPACE/.chia + echo "$HOME/.chia" + ls -al $HOME/.chia + + - name: Install ubuntu dependencies + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh + + - name: Install timelord + run: | + . ./activate + sh install-timelord.sh + ./vdf_bench square_asm 400000 + + - name: Install developer requirements + run: | + . ./activate + venv/bin/python -m pip install pytest pytest-asyncio pytest-xdist + + - name: Test core-daemon code with pytest + run: | + . ./activate + ./venv/bin/py.test tests/core/daemon/test_daemon.py -s -v --durations 0 diff --git a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml index 350b7ccf5cb2..bf5dc2d075af 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node-full_sync.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-core-full_node.yml b/.github/workflows/build-test-ubuntu-core-full_node.yml index e422a1796ab0..0a585fd7a3b6 100644 --- a/.github/workflows/build-test-ubuntu-core-full_node.yml +++ b/.github/workflows/build-test-ubuntu-core-full_node.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -102,7 +102,7 @@ jobs: - name: Test core-full_node code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/full_node/test_address_manager.py tests/core/full_node/test_block_store.py tests/core/full_node/test_coin_store.py tests/core/full_node/test_full_node.py tests/core/full_node/test_full_node_store.py tests/core/full_node/test_initial_freeze.py tests/core/full_node/test_mempool.py tests/core/full_node/test_mempool_performance.py tests/core/full_node/test_node_load.py tests/core/full_node/test_sync_store.py tests/core/full_node/test_transactions.py -s -v --durations 0 + ./venv/bin/py.test tests/core/full_node/test_address_manager.py tests/core/full_node/test_block_store.py tests/core/full_node/test_coin_store.py tests/core/full_node/test_conditions.py tests/core/full_node/test_full_node.py tests/core/full_node/test_full_node_store.py tests/core/full_node/test_initial_freeze.py tests/core/full_node/test_mempool.py tests/core/full_node/test_mempool_performance.py tests/core/full_node/test_node_load.py tests/core/full_node/test_performance.py tests/core/full_node/test_sync_store.py tests/core/full_node/test_transactions.py -s -v --durations 0 - name: Check resource usage run: | sqlite3 -readonly -separator " " .pymon "select item,cpu_usage,total_time,mem_usage from TEST_METRICS order by mem_usage desc;" >metrics.out diff --git a/.github/workflows/build-test-ubuntu-core-server.yml b/.github/workflows/build-test-ubuntu-core-server.yml index 3108e69c796e..0ed3c47cdc2b 100644 --- a/.github/workflows/build-test-ubuntu-core-server.yml +++ b/.github/workflows/build-test-ubuntu-core-server.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-core-ssl.yml b/.github/workflows/build-test-ubuntu-core-ssl.yml index beaa917a6fec..ae554f48f016 100644 --- a/.github/workflows/build-test-ubuntu-core-ssl.yml +++ b/.github/workflows/build-test-ubuntu-core-ssl.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-core-types.yml b/.github/workflows/build-test-ubuntu-core-types.yml index 273d94f32c79..b38aee97cb29 100644 --- a/.github/workflows/build-test-ubuntu-core-types.yml +++ b/.github/workflows/build-test-ubuntu-core-types.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -102,4 +102,4 @@ jobs: - name: Test core-types code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/types/test_proof_of_space.py -s -v --durations 0 + ./venv/bin/py.test tests/core/types/test_coin.py tests/core/types/test_proof_of_space.py -s -v --durations 0 diff --git a/.github/workflows/build-test-ubuntu-core-util.yml b/.github/workflows/build-test-ubuntu-core-util.yml index 0c7b1945b685..f4a84b5a0ee0 100644 --- a/.github/workflows/build-test-ubuntu-core-util.yml +++ b/.github/workflows/build-test-ubuntu-core-util.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -102,4 +102,4 @@ jobs: - name: Test core-util code with pytest run: | . ./activate - ./venv/bin/py.test tests/core/util/test_keychain.py tests/core/util/test_significant_bits.py tests/core/util/test_streamable.py tests/core/util/test_type_checking.py -s -v --durations 0 + ./venv/bin/py.test tests/core/util/test_keychain.py tests/core/util/test_lru_cache.py tests/core/util/test_significant_bits.py tests/core/util/test_streamable.py tests/core/util/test_type_checking.py -s -v --durations 0 diff --git a/.github/workflows/build-test-ubuntu-core.yml b/.github/workflows/build-test-ubuntu-core.yml index aa2ee89c6bd5..cb5312790ed4 100644 --- a/.github/workflows/build-test-ubuntu-core.yml +++ b/.github/workflows/build-test-ubuntu-core.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-generator.yml b/.github/workflows/build-test-ubuntu-generator.yml index 2c4ca83d5d56..74f69767d385 100644 --- a/.github/workflows/build-test-ubuntu-generator.yml +++ b/.github/workflows/build-test-ubuntu-generator.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -102,4 +102,4 @@ jobs: - name: Test generator code with pytest run: | . ./activate - ./venv/bin/py.test tests/generator/test_compression.py tests/generator/test_generator_types.py tests/generator/test_scan.py -s -v --durations 0 + ./venv/bin/py.test tests/generator/test_compression.py tests/generator/test_generator_types.py tests/generator/test_rom.py tests/generator/test_scan.py -s -v --durations 0 diff --git a/.github/workflows/build-test-ubuntu-pools.yml b/.github/workflows/build-test-ubuntu-pools.yml new file mode 100644 index 000000000000..7edfda86c103 --- /dev/null +++ b/.github/workflows/build-test-ubuntu-pools.yml @@ -0,0 +1,105 @@ +name: Ubuntu pools Test + +on: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + name: Ubuntu pools Test + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + matrix: + python-version: [3.7, 3.8, 3.9] + os: [ubuntu-latest] + + steps: + - name: Cancel previous runs on the same branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: styfle/cancel-workflow-action@0.9.0 + with: + access_token: ${{ github.token }} + + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache npm + uses: actions/cache@v2.1.6 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache pip + uses: actions/cache@v2.1.6 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Checkout test blocks and plots + uses: actions/checkout@v2 + with: + repository: 'Chia-Network/test-cache' + path: '.chia' + ref: '0.27.0' + fetch-depth: 1 + + - name: Link home directory + run: | + cd $HOME + ln -s $GITHUB_WORKSPACE/.chia + echo "$HOME/.chia" + ls -al $HOME/.chia + + - name: Install ubuntu dependencies + run: | + sudo apt-get install software-properties-common + sudo add-apt-repository ppa:deadsnakes/ppa + sudo apt-get update + sudo apt-get install python${{ matrix.python-version }}-venv python${{ matrix.python-version }}-distutils git -y + + - name: Run install script + env: + INSTALL_PYTHON_VERSION: ${{ matrix.python-version }} + run: | + sh install.sh + + - name: Install timelord + run: | + . ./activate + sh install-timelord.sh + ./vdf_bench square_asm 400000 + + - name: Install developer requirements + run: | + . ./activate + venv/bin/python -m pip install pytest pytest-asyncio pytest-xdist + + - name: Test pools code with pytest + run: | + . ./activate + ./venv/bin/py.test tests/pools/test_pool_config.py tests/pools/test_pool_puzzles_lifecycle.py tests/pools/test_pool_rpc.py tests/pools/test_pool_wallet.py tests/pools/test_wallet_pool_store.py -s -v --durations 0 diff --git a/.github/workflows/build-test-ubuntu-simulation.yml b/.github/workflows/build-test-ubuntu-simulation.yml index bed4c6307b6f..ca0cfe65cb3a 100644 --- a/.github/workflows/build-test-ubuntu-simulation.yml +++ b/.github/workflows/build-test-ubuntu-simulation.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-wallet-cc_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-cc_wallet.yml index 7a89df836795..ed06f0f4beb4 100644 --- a/.github/workflows/build-test-ubuntu-wallet-cc_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-cc_wallet.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml index 2e323a01b8d8..4f1716d96f3e 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rl_wallet.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-wallet-rpc.yml b/.github/workflows/build-test-ubuntu-wallet-rpc.yml index b1a9581723b7..2cf0906ab005 100644 --- a/.github/workflows/build-test-ubuntu-wallet-rpc.yml +++ b/.github/workflows/build-test-ubuntu-wallet-rpc.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-wallet-sync.yml b/.github/workflows/build-test-ubuntu-wallet-sync.yml index 60d07134587c..5f3950b6282d 100644 --- a/.github/workflows/build-test-ubuntu-wallet-sync.yml +++ b/.github/workflows/build-test-ubuntu-wallet-sync.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory diff --git a/.github/workflows/build-test-ubuntu-wallet.yml b/.github/workflows/build-test-ubuntu-wallet.yml index 89dfa267f5da..65371c3ed433 100644 --- a/.github/workflows/build-test-ubuntu-wallet.yml +++ b/.github/workflows/build-test-ubuntu-wallet.yml @@ -65,7 +65,7 @@ jobs: with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 - name: Link home directory @@ -102,4 +102,4 @@ jobs: - name: Test wallet code with pytest run: | . ./activate - ./venv/bin/py.test tests/wallet/test_backup.py tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_store.py -s -v --durations 0 + ./venv/bin/py.test tests/wallet/test_backup.py tests/wallet/test_bech32m.py tests/wallet/test_chialisp.py tests/wallet/test_puzzle_store.py tests/wallet/test_singleton.py tests/wallet/test_singleton_lifecycle.py tests/wallet/test_singleton_lifecycle_fast.py tests/wallet/test_taproot.py tests/wallet/test_wallet.py tests/wallet/test_wallet_interested_store.py tests/wallet/test_wallet_store.py -s -v --durations 0 diff --git a/.gitmodules b/.gitmodules index 596e3d8820f0..0f84cbc8b049 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ [submodule "chia-blockchain-gui"] path = chia-blockchain-gui url = https://github.com/Chia-Network/chia-blockchain-gui.git - branch = main + branch = pools [submodule "mozilla-ca"] path = mozilla-ca url = https://github.com/Chia-Network/mozilla-ca.git diff --git a/build_scripts/build_windows.ps1 b/build_scripts/build_windows.ps1 index 6d750bef7a33..69bc77be376a 100644 --- a/build_scripts/build_windows.ps1 +++ b/build_scripts/build_windows.ps1 @@ -80,6 +80,7 @@ git status Write-Output " ---" Write-Output "Prepare Electron packager" Write-Output " ---" +$Env:NODE_OPTIONS = "--max-old-space-size=3000" npm install --save-dev electron-winstaller npm install -g electron-packager npm install diff --git a/chia-blockchain-gui b/chia-blockchain-gui index 444c6966fe50..aee851c839e1 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit 444c6966fe50183c8d72cbc972c5403db341739c +Subproject commit aee851c839e1b0d4a8c8d8d2265cffce94ff2481 diff --git a/chia/clvm/singleton.py b/chia/clvm/singleton.py new file mode 100644 index 000000000000..21b347784dae --- /dev/null +++ b/chia/clvm/singleton.py @@ -0,0 +1,5 @@ +from chia.wallet.puzzles.load_clvm import load_clvm + +P2_SINGLETON_MOD = load_clvm("p2_singleton.clvm") +SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer.clvm") +SINGLETON_LAUNCHER = load_clvm("singleton_launcher.clvm") diff --git a/chia/cmds/chia.py b/chia/cmds/chia.py index 17fe565887f7..7cac4e056030 100644 --- a/chia/cmds/chia.py +++ b/chia/cmds/chia.py @@ -11,6 +11,7 @@ from chia.cmds.show import show_cmd from chia.cmds.start import start_cmd from chia.cmds.stop import stop_cmd from chia.cmds.wallet import wallet_cmd +from chia.cmds.plotnft import plotnft_cmd from chia.util.default_root import DEFAULT_ROOT_PATH CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) @@ -63,6 +64,7 @@ def run_daemon_cmd(ctx: click.Context) -> None: cli.add_command(keys_cmd) cli.add_command(plots_cmd) cli.add_command(wallet_cmd) +cli.add_command(plotnft_cmd) cli.add_command(configure_cmd) cli.add_command(init_cmd) cli.add_command(show_cmd) diff --git a/chia/cmds/farm_funcs.py b/chia/cmds/farm_funcs.py index 1e42e57729d3..9978f5fab2cb 100644 --- a/chia/cmds/farm_funcs.py +++ b/chia/cmds/farm_funcs.py @@ -6,7 +6,6 @@ from chia.cmds.units import units from chia.consensus.block_record import BlockRecord from chia.rpc.farmer_rpc_client import FarmerRpcClient from chia.rpc.full_node_rpc_client import FullNodeRpcClient -from chia.rpc.harvester_rpc_client import HarvesterRpcClient from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH @@ -17,25 +16,22 @@ from chia.util.misc import format_minutes SECONDS_PER_BLOCK = (24 * 3600) / 4608 -async def get_plots(harvester_rpc_port: int) -> Optional[Dict[str, Any]]: - plots = None +async def get_plots(farmer_rpc_port: int) -> Optional[Dict[str, Any]]: try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") self_hostname = config["self_hostname"] - if harvester_rpc_port is None: - harvester_rpc_port = config["harvester"]["rpc_port"] - harvester_client = await HarvesterRpcClient.create( - self_hostname, uint16(harvester_rpc_port), DEFAULT_ROOT_PATH, config - ) - plots = await harvester_client.get_plots() + if farmer_rpc_port is None: + farmer_rpc_port = config["farmer"]["rpc_port"] + farmer_client = await FarmerRpcClient.create(self_hostname, uint16(farmer_rpc_port), DEFAULT_ROOT_PATH, config) + plots = await farmer_client.get_plots() except Exception as e: if isinstance(e, aiohttp.ClientConnectorError): - print(f"Connection error. Check if harvester is running at {harvester_rpc_port}") + print(f"Connection error. Check if farmer is running at {farmer_rpc_port}") else: print(f"Exception from 'harvester' {e}") - - harvester_client.close() - await harvester_client.await_closed() + return None + farmer_client.close() + await farmer_client.await_closed() return plots @@ -182,7 +178,7 @@ async def challenges(farmer_rpc_port: int, limit: int) -> None: async def summary(rpc_port: int, wallet_rpc_port: int, harvester_rpc_port: int, farmer_rpc_port: int) -> None: - plots = await get_plots(harvester_rpc_port) + all_plots = await get_plots(farmer_rpc_port) blockchain_state = await get_blockchain_state(rpc_port) farmer_running = await is_farmer_running(farmer_rpc_port) @@ -216,10 +212,19 @@ async def summary(rpc_port: int, wallet_rpc_port: int, harvester_rpc_port: int, print(f"Last height farmed: {amounts['last_height_farmed']}") total_plot_size = 0 - if plots is not None: - total_plot_size = sum(map(lambda x: x["file_size"], plots["plots"])) + total_plots = 0 + if all_plots is not None: + for harvester_ip, plots in all_plots.items(): + if harvester_ip == "success": + # This key is just "success": True + continue + total_plot_size_harvester = sum(map(lambda x: x["file_size"], plots["plots"])) + total_plot_size += total_plot_size_harvester + total_plots += len(plots["plots"]) + print(f"Harvester {harvester_ip}:") + print(f" {len(plots['plots'])} plots of size: {format_bytes(total_plot_size_harvester)}") - print(f"Plot count: {len(plots['plots'])}") + print(f"Plot count for all harvesters: {total_plots}") print("Total size of plots: ", end="") print(format_bytes(total_plot_size)) @@ -234,11 +239,11 @@ async def summary(rpc_port: int, wallet_rpc_port: int, harvester_rpc_port: int, print("Estimated network space: Unknown") minutes = -1 - if blockchain_state is not None and plots is not None: + if blockchain_state is not None and all_plots is not None: proportion = total_plot_size / blockchain_state["space"] if blockchain_state["space"] else -1 minutes = int((await get_average_block_time(rpc_port) / 60) / proportion) if proportion else -1 - if plots is not None and len(plots["plots"]) == 0: + if all_plots is not None and total_plots == 0: print("Expected time to win: Never (no plots)") else: print("Expected time to win: " + format_minutes(minutes)) diff --git a/chia/cmds/keys_funcs.py b/chia/cmds/keys_funcs.py index 0a821c9c9dc0..fafe21aaa14b 100644 --- a/chia/cmds/keys_funcs.py +++ b/chia/cmds/keys_funcs.py @@ -49,7 +49,7 @@ def add_private_key_seed(mnemonic: str): passphrase = "" sk = keychain.add_private_key(mnemonic, passphrase) fingerprint = sk.get_g1().get_fingerprint() - print(f"Added private key with public key fingerprint {fingerprint} and mnemonic") + print(f"Added private key with public key fingerprint {fingerprint}") except ValueError as e: print(e) diff --git a/chia/cmds/plotnft.py b/chia/cmds/plotnft.py new file mode 100644 index 000000000000..ef637a2ea1c0 --- /dev/null +++ b/chia/cmds/plotnft.py @@ -0,0 +1,142 @@ +import click + + +@click.group("plotnft", short_help="Manage your plot NFTs") +def plotnft_cmd() -> None: + pass + + +@plotnft_cmd.command("show", short_help="Show plotnft information") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=False) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +def show_cmd(wallet_rpc_port: int, fingerprint: int, id: int) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet + from .plotnft_funcs import show + + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, {"id": id}, show)) + + +@plotnft_cmd.command( + "get_login_link", short_help="Create a login link for a pool. To get the launcher id, use plotnft show." +) +@click.option("-l", "--launcher_id", help="Launcher ID of the plotnft", type=str, required=True) +def get_login_link_cmd(launcher_id: str) -> None: + import asyncio + from .plotnft_funcs import get_login_link + + asyncio.run(get_login_link(launcher_id)) + + +@plotnft_cmd.command("create", short_help="Create a plot NFT") +@click.option("-y", "--yes", help="No prompts", is_flag=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +@click.option("-u", "--pool_url", help="HTTPS host:port of the pool to join", type=str, required=False) +@click.option("-s", "--state", help="Initial state of Plot NFT: local or pool", type=str, required=True) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +def create_cmd(wallet_rpc_port: int, fingerprint: int, pool_url: str, state: str, yes: bool) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet + from .plotnft_funcs import create + + if pool_url is not None and state.lower() == "local": + print(f" pool_url argument [{pool_url}] is not allowed when creating in 'local' state") + return + if pool_url in [None, ""] and state.lower() == "pool": + print(" pool_url argument (-u) is required for pool starting state") + return + valid_initial_states = {"pool": "FARMING_TO_POOL", "local": "SELF_POOLING"} + extra_params = {"pool_url": pool_url, "state": valid_initial_states[state], "yes": yes} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create)) + + +@plotnft_cmd.command("join", short_help="Join a plot NFT to a Pool") +@click.option("-y", "--yes", help="No prompts", is_flag=True) +@click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +@click.option("-u", "--pool_url", help="HTTPS host:port of the pool to join", type=str, required=True) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +def join_cmd(wallet_rpc_port: int, fingerprint: int, id: int, pool_url: str, yes: bool) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet + from .plotnft_funcs import join_pool + + extra_params = {"pool_url": pool_url, "id": id, "yes": yes} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, join_pool)) + + +@plotnft_cmd.command("leave", short_help="Make a plot NFT and return to self-farming") +@click.option("-y", "--yes", help="No prompts", is_flag=True) +@click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +def self_pool_cmd(wallet_rpc_port: int, fingerprint: int, id: int, yes: bool) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet + from .plotnft_funcs import self_pool + + extra_params = {"id": id, "yes": yes} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, self_pool)) + + +@plotnft_cmd.command("inspect", short_help="Get Detailed plotnft information as JSON") +@click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +def inspect(wallet_rpc_port: int, fingerprint: int, id: int) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet + from .plotnft_funcs import inspect_cmd + + extra_params = {"id": id} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, inspect_cmd)) + + +@plotnft_cmd.command("claim", short_help="Claim rewards from a plot NFT") +@click.option("-i", "--id", help="ID of the wallet to use", type=int, default=None, show_default=True, required=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +def claim(wallet_rpc_port: int, fingerprint: int, id: int) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet + from .plotnft_funcs import claim_cmd + + extra_params = {"id": id} + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, claim_cmd)) diff --git a/chia/cmds/plotnft_funcs.py b/chia/cmds/plotnft_funcs.py new file mode 100644 index 000000000000..b40a5050f432 --- /dev/null +++ b/chia/cmds/plotnft_funcs.py @@ -0,0 +1,327 @@ +import aiohttp +import asyncio +import functools +import json +import time + +from pprint import pprint +from typing import List, Dict, Optional, Callable + +from chia.cmds.wallet_funcs import print_balance, wallet_coin_unit +from chia.pools.pool_wallet_info import PoolWalletInfo, PoolSingletonState +from chia.protocols.pool_protocol import POOL_PROTOCOL_VERSION +from chia.rpc.farmer_rpc_client import FarmerRpcClient +from chia.rpc.wallet_rpc_client import WalletRpcClient +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.bech32m import encode_puzzle_hash +from chia.util.byte_types import hexstr_to_bytes +from chia.util.config import load_config +from chia.util.default_root import DEFAULT_ROOT_PATH +from chia.util.ints import uint16, uint32 +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.wallet_types import WalletType + + +async def create_pool_args(pool_url: str) -> Dict: + try: + async with aiohttp.ClientSession() as session: + async with session.get(f"{pool_url}/pool_info") as response: + if response.ok: + json_dict = json.loads(await response.text()) + else: + raise ValueError(f"Response from {pool_url} not OK: {response.status}") + except Exception as e: + raise ValueError(f"Error connecting to pool {pool_url}: {e}") + + if json_dict["relative_lock_height"] > 1000: + raise ValueError("Relative lock height too high for this pool, cannot join") + if json_dict["protocol_version"] != POOL_PROTOCOL_VERSION: + raise ValueError(f"Incorrect version: {json_dict['protocol_version']}, should be {POOL_PROTOCOL_VERSION}") + + header_msg = f"\n---- Pool parameters fetched from {pool_url} ----" + print(header_msg) + pprint(json_dict) + print("-" * len(header_msg)) + return json_dict + + +async def create(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + state = args["state"] + prompt = not args.get("yes", False) + + # Could use initial_pool_state_from_dict to simplify + if state == "SELF_POOLING": + pool_url: Optional[str] = None + relative_lock_height = uint32(0) + target_puzzle_hash = None # wallet will fill this in + elif state == "FARMING_TO_POOL": + pool_url = str(args["pool_url"]) + json_dict = await create_pool_args(pool_url) + relative_lock_height = json_dict["relative_lock_height"] + target_puzzle_hash = hexstr_to_bytes(json_dict["target_puzzle_hash"]) + else: + raise ValueError("Plot NFT must be created in SELF_POOLING or FARMING_TO_POOL state.") + + pool_msg = f" and join pool: {pool_url}" if pool_url else "" + print(f"Will create a plot NFT{pool_msg}.") + if prompt: + user_input: str = input("Confirm [n]/y: ") + else: + user_input = "yes" + + if user_input.lower() == "y" or user_input.lower() == "yes": + try: + tx_record: TransactionRecord = await wallet_client.create_new_pool_wallet( + target_puzzle_hash, + pool_url, + relative_lock_height, + "localhost:5000", + "new", + state, + ) + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(str(1), tx_record.name) + if len(tx.sent_to) > 0: + print(f"Transaction submitted to nodes: {tx.sent_to}") + print(f"Do chia wallet get_transaction -f {fingerprint} -tx 0x{tx_record.name} to get status") + return None + except Exception as e: + print(f"Error creating plot NFT: {e}") + return + print("Aborting.") + + +async def pprint_pool_wallet_state( + wallet_client: WalletRpcClient, + wallet_id: int, + pool_wallet_info: PoolWalletInfo, + address_prefix: str, + pool_state_dict: Dict, + unconfirmed_transactions: List[TransactionRecord], +): + if pool_wallet_info.current.state == PoolSingletonState.LEAVING_POOL and pool_wallet_info.target is None: + expected_leave_height = pool_wallet_info.singleton_block_height + pool_wallet_info.current.relative_lock_height + print(f"Current state: INVALID_STATE. Please leave/join again after block height {expected_leave_height}") + else: + print(f"Current state: {PoolSingletonState(pool_wallet_info.current.state).name}") + print(f"Current state from block height: {pool_wallet_info.singleton_block_height}") + print(f"Launcher ID: {pool_wallet_info.launcher_id}") + print( + "Target address (not for plotting): " + f"{encode_puzzle_hash(pool_wallet_info.current.target_puzzle_hash, address_prefix)}" + ) + print(f"Owner public key: {pool_wallet_info.current.owner_pubkey}") + + print( + f"P2 singleton address (pool contract address for plotting): " + f"{encode_puzzle_hash(pool_wallet_info.p2_singleton_puzzle_hash, address_prefix)}" + ) + if pool_wallet_info.target is not None: + print(f"Target state: {PoolSingletonState(pool_wallet_info.target.state).name}") + print(f"Target pool URL: {pool_wallet_info.target.pool_url}") + if pool_wallet_info.current.state == PoolSingletonState.SELF_POOLING.value: + balances: Dict = await wallet_client.get_wallet_balance(str(wallet_id)) + balance = balances["confirmed_wallet_balance"] + typ = WalletType(int(WalletType.POOLING_WALLET)) + address_prefix, scale = wallet_coin_unit(typ, address_prefix) + print(f"Claimable balance: {print_balance(balance, scale, address_prefix)}") + if pool_wallet_info.current.state == PoolSingletonState.FARMING_TO_POOL: + print(f"Current pool URL: {pool_wallet_info.current.pool_url}") + if pool_wallet_info.launcher_id in pool_state_dict: + print(f"Current difficulty: {pool_state_dict[pool_wallet_info.launcher_id]['current_difficulty']}") + print(f"Points balance: {pool_state_dict[pool_wallet_info.launcher_id]['current_points']}") + print(f"Relative lock height: {pool_wallet_info.current.relative_lock_height} blocks") + payout_instructions: str = pool_state_dict[pool_wallet_info.launcher_id]["pool_config"]["payout_instructions"] + try: + payout_address = encode_puzzle_hash(bytes32.fromhex(payout_instructions), address_prefix) + print(f"Payout instructions (pool will pay to this address): {payout_address}") + except Exception: + print(f"Payout instructions (pool will pay you with this): {payout_instructions}") + if pool_wallet_info.current.state == PoolSingletonState.LEAVING_POOL: + expected_leave_height = pool_wallet_info.singleton_block_height + pool_wallet_info.current.relative_lock_height + if pool_wallet_info.target is not None: + print(f"Expected to leave after block height: {expected_leave_height}") + + +async def show(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") + self_hostname = config["self_hostname"] + farmer_rpc_port = config["farmer"]["rpc_port"] + farmer_client = await FarmerRpcClient.create(self_hostname, uint16(farmer_rpc_port), DEFAULT_ROOT_PATH, config) + address_prefix = config["network_overrides"]["config"][config["selected_network"]]["address_prefix"] + summaries_response = await wallet_client.get_wallets() + wallet_id_passed_in = args.get("id", None) + try: + pool_state_list: List = (await farmer_client.get_pool_state())["pool_state"] + except Exception as e: + if isinstance(e, aiohttp.ClientConnectorError): + print( + f"Connection error. Check if farmer is running at {farmer_rpc_port}." + f" You can run the farmer by:\n chia start farmer-only" + ) + else: + print(f"Exception from 'wallet' {e}") + farmer_client.close() + await farmer_client.await_closed() + return + pool_state_dict: Dict[bytes32, Dict] = { + hexstr_to_bytes(pool_state_item["pool_config"]["launcher_id"]): pool_state_item + for pool_state_item in pool_state_list + } + if wallet_id_passed_in is not None: + for summary in summaries_response: + typ = WalletType(int(summary["type"])) + if summary["id"] == wallet_id_passed_in and typ != WalletType.POOLING_WALLET: + print(f"Wallet with id: {wallet_id_passed_in} is not a pooling wallet. Please provide a different id.") + return + pool_wallet_info, unconfirmed_transactions = await wallet_client.pw_status(wallet_id_passed_in) + await pprint_pool_wallet_state( + wallet_client, + wallet_id_passed_in, + pool_wallet_info, + address_prefix, + pool_state_dict, + unconfirmed_transactions, + ) + else: + print(f"Wallet height: {await wallet_client.get_height_info()}") + print(f"Sync status: {'Synced' if (await wallet_client.get_synced()) else 'Not synced'}") + for summary in summaries_response: + wallet_id = summary["id"] + typ = WalletType(int(summary["type"])) + if typ == WalletType.POOLING_WALLET: + print(f"Wallet id {wallet_id}: ") + pool_wallet_info, unconfirmed_transactions = await wallet_client.pw_status(wallet_id) + await pprint_pool_wallet_state( + wallet_client, + wallet_id, + pool_wallet_info, + address_prefix, + pool_state_dict, + unconfirmed_transactions, + ) + print("") + farmer_client.close() + await farmer_client.await_closed() + + +async def get_login_link(launcher_id_str: str) -> None: + launcher_id: bytes32 = hexstr_to_bytes(launcher_id_str) + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") + self_hostname = config["self_hostname"] + farmer_rpc_port = config["farmer"]["rpc_port"] + farmer_client = await FarmerRpcClient.create(self_hostname, uint16(farmer_rpc_port), DEFAULT_ROOT_PATH, config) + try: + login_link: Optional[str] = await farmer_client.get_pool_login_link(launcher_id) + if login_link is None: + print("Was not able to get login link.") + else: + print(login_link) + except Exception as e: + if isinstance(e, aiohttp.ClientConnectorError): + print( + f"Connection error. Check if farmer is running at {farmer_rpc_port}." + f" You can run the farmer by:\n chia start farmer-only" + ) + else: + print(f"Exception from 'farmer' {e}") + finally: + farmer_client.close() + await farmer_client.await_closed() + + +async def submit_tx_with_confirmation( + message: str, prompt: bool, func: Callable, wallet_client: WalletRpcClient, fingerprint: int, wallet_id: int +): + print(message) + if prompt: + user_input: str = input("Confirm [n]/y: ") + else: + user_input = "yes" + + if user_input.lower() == "y" or user_input.lower() == "yes": + try: + tx_record: TransactionRecord = await func() + start = time.time() + while time.time() - start < 10: + await asyncio.sleep(0.1) + tx = await wallet_client.get_transaction(str(1), tx_record.name) + if len(tx.sent_to) > 0: + print(f"Transaction submitted to nodes: {tx.sent_to}") + print(f"Do chia wallet get_transaction -f {fingerprint} -tx 0x{tx_record.name} to get status") + return None + except Exception as e: + print(f"Error performing operation on Plot NFT -f {fingerprint} wallet id: {wallet_id}: {e}") + return + print("Aborting.") + + +async def join_pool(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + pool_url = args["pool_url"] + wallet_id = args.get("id", None) + prompt = not args.get("yes", False) + try: + async with aiohttp.ClientSession() as session: + async with session.get(f"{pool_url}/pool_info") as response: + if response.ok: + json_dict = json.loads(await response.text()) + else: + print(f"Response not OK: {response.status}") + return + except Exception as e: + print(f"Error connecting to pool {pool_url}: {e}") + return + + if json_dict["relative_lock_height"] > 1000: + print("Relative lock height too high for this pool, cannot join") + return + if json_dict["protocol_version"] != POOL_PROTOCOL_VERSION: + print(f"Incorrect version: {json_dict['protocol_version']}, should be {POOL_PROTOCOL_VERSION}") + return + + pprint(json_dict) + msg = f"\nWill join pool: {pool_url} with Plot NFT {fingerprint}." + func = functools.partial( + wallet_client.pw_join_pool, + wallet_id, + hexstr_to_bytes(json_dict["target_puzzle_hash"]), + pool_url, + json_dict["relative_lock_height"], + ) + + await submit_tx_with_confirmation(msg, prompt, func, wallet_client, fingerprint, wallet_id) + + +async def self_pool(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args.get("id", None) + prompt = not args.get("yes", False) + + msg = f"Will start self-farming with Plot NFT on wallet id {wallet_id} fingerprint {fingerprint}." + func = functools.partial(wallet_client.pw_self_pool, wallet_id) + await submit_tx_with_confirmation(msg, prompt, func, wallet_client, fingerprint, wallet_id) + + +async def inspect_cmd(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args.get("id", None) + pool_wallet_info, unconfirmed_transactions = await wallet_client.pw_status(wallet_id) + print( + { + "pool_wallet_info": pool_wallet_info, + "unconfirmed_transactions": [ + {"sent_to": tx.sent_to, "transaction_id": tx.name.hex()} for tx in unconfirmed_transactions + ], + } + ) + + +async def claim_cmd(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args.get("id", None) + msg = f"\nWill claim rewards for wallet ID: {wallet_id}." + func = functools.partial( + wallet_client.pw_absorb_rewards, + wallet_id, + ) + await submit_tx_with_confirmation(msg, False, func, wallet_client, fingerprint, wallet_id) diff --git a/chia/cmds/show.py b/chia/cmds/show.py index d698a0f4167f..d26e3ddb1d86 100644 --- a/chia/cmds/show.py +++ b/chia/cmds/show.py @@ -50,18 +50,14 @@ async def show_async( total_iters = peak.total_iters if peak is not None else 0 num_blocks: int = 10 - if sync_mode: - sync_max_block = blockchain_state["sync"]["sync_tip_height"] - sync_current_block = blockchain_state["sync"]["sync_progress_height"] - print( - "Current Blockchain Status: Full Node syncing to block", - sync_max_block, - "\nCurrently synced to block:", - sync_current_block, - ) if synced: print("Current Blockchain Status: Full Node Synced") print("\nPeak: Hash:", peak.header_hash if peak is not None else "") + elif peak is not None and sync_mode: + sync_max_block = blockchain_state["sync"]["sync_tip_height"] + sync_current_block = blockchain_state["sync"]["sync_progress_height"] + print(f"Current Blockchain Status: Syncing {sync_current_block}/{sync_max_block}.") + print("Peak: Hash:", peak.header_hash if peak is not None else "") elif peak is not None: print(f"Current Blockchain Status: Not Synced. Peak height: {peak.height}") else: diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 0161ce1325ef..d90657295167 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -120,3 +120,23 @@ def get_address_cmd(wallet_rpc_port: int, id, fingerprint: int) -> None: from .wallet_funcs import execute_with_wallet, get_address asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, get_address)) + + +@wallet_cmd.command( + "delete_unconfirmed_transactions", short_help="Deletes all unconfirmed transactions for this wallet ID" +) +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@click.option("-i", "--id", help="Id of the wallet to use", type=int, default=1, show_default=True, required=True) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +def delete_unconfirmed_transactions_cmd(wallet_rpc_port: int, id, fingerprint: int) -> None: + extra_params = {"id": id} + import asyncio + from .wallet_funcs import execute_with_wallet, delete_unconfirmed_transactions + + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, delete_unconfirmed_transactions)) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index bd795cacbedc..2af16dff5f18 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -109,6 +109,27 @@ async def get_address(args: dict, wallet_client: WalletRpcClient, fingerprint: i print(res) +async def delete_unconfirmed_transactions(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["id"] + await wallet_client.delete_unconfirmed_transactions(wallet_id) + print(f"Successfully deleted all unconfirmed transactions for wallet id {wallet_id} on key {fingerprint}") + + +def wallet_coin_unit(typ: WalletType, address_prefix: str) -> Tuple[str, int]: + if typ == WalletType.COLOURED_COIN: + return "", units["colouredcoin"] + if typ in [WalletType.STANDARD_WALLET, WalletType.POOLING_WALLET, WalletType.MULTI_SIG, WalletType.RATE_LIMITED]: + return address_prefix, units["chia"] + return "", units["mojo"] + + +def print_balance(amount: int, scale: int, address_prefix: str) -> str: + ret = f"{amount/scale} {address_prefix} " + if scale > 1: + ret += f"({amount} mojo)" + return ret + + async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: summaries_response = await wallet_client.get_wallets() config = load_config(DEFAULT_ROOT_PATH, "config.yaml") @@ -120,26 +141,14 @@ async def print_balances(args: dict, wallet_client: WalletRpcClient, fingerprint for summary in summaries_response: wallet_id = summary["id"] balances = await wallet_client.get_wallet_balance(wallet_id) - typ = WalletType(int(summary["type"])).name - if typ != "STANDARD_WALLET": - print(f"Wallet ID {wallet_id} type {typ} {summary['name']}") - print(f" -Total Balance: " f"{balances['confirmed_wallet_balance']/units['colouredcoin']}") - print(f" -Pending Total Balance: {balances['unconfirmed_wallet_balance']/units['colouredcoin']}") - print(f" -Spendable Balance: {balances['spendable_balance']/units['colouredcoin']}") - else: - print(f"Wallet ID {wallet_id} type {typ}") - print( - f" -Total Balance: {balances['confirmed_wallet_balance']/units['chia']} {address_prefix} " - f"({balances['confirmed_wallet_balance']} mojo)" - ) - print( - f" -Pending Total Balance: {balances['unconfirmed_wallet_balance']/units['chia']} {address_prefix} " - f"({balances['unconfirmed_wallet_balance']} mojo)" - ) - print( - f" -Spendable: {balances['spendable_balance']/units['chia']} {address_prefix} " - f"({balances['spendable_balance']} mojo)" - ) + typ = WalletType(int(summary["type"])) + address_prefix, scale = wallet_coin_unit(typ, address_prefix) + print(f"Wallet ID {wallet_id} type {typ.name} {summary['name']}") + print(f" -Total Balance: {print_balance(balances['confirmed_wallet_balance'], scale, address_prefix)}") + print( + f" -Pending Total Balance: {print_balance(balances['unconfirmed_wallet_balance'], scale, address_prefix)}" + ) + print(f" -Spendable: {print_balance(balances['spendable_balance'], scale, address_prefix)}") async def get_wallet(wallet_client: WalletRpcClient, fingerprint: int = None) -> Optional[Tuple[WalletRpcClient, int]]: @@ -232,7 +241,10 @@ async def execute_with_wallet(wallet_rpc_port: int, fingerprint: int, extra_para pass except Exception as e: if isinstance(e, aiohttp.ClientConnectorError): - print(f"Connection error. Check if wallet is running at {wallet_rpc_port}") + print( + f"Connection error. Check if the wallet is running at {wallet_rpc_port}. " + "You can run the wallet via:\n\tchia start wallet" + ) else: print(f"Exception from 'wallet' {e}") wallet_client.close() diff --git a/chia/consensus/block_body_validation.py b/chia/consensus/block_body_validation.py index cfeb28d98643..6d8345ead43a 100644 --- a/chia/consensus/block_body_validation.py +++ b/chia/consensus/block_body_validation.py @@ -388,6 +388,7 @@ async def validate_block_body( # This coin is not in the current heaviest chain, so it must be in the fork if rem not in additions_since_fork: # Check for spending a coin that does not exist in this fork + log.error(f"Err.UNKNOWN_UNSPENT: COIN ID: {rem} NPC RESULT: {npc_result}") return Err.UNKNOWN_UNSPENT, None new_coin, confirmed_height, confirmed_timestamp = additions_since_fork[rem] new_coin_record: CoinRecord = CoinRecord( diff --git a/chia/consensus/coinbase.py b/chia/consensus/coinbase.py index 7f58b329764f..40cde5cc85e0 100644 --- a/chia/consensus/coinbase.py +++ b/chia/consensus/coinbase.py @@ -10,7 +10,7 @@ def create_puzzlehash_for_pk(pub_key: G1Element) -> bytes32: return puzzle_for_pk(pub_key).get_tree_hash() -def pool_parent_id(block_height: uint32, genesis_challenge: bytes32) -> uint32: +def pool_parent_id(block_height: uint32, genesis_challenge: bytes32) -> bytes32: return bytes32(genesis_challenge[:16] + block_height.to_bytes(16, "big")) diff --git a/chia/consensus/constants.py b/chia/consensus/constants.py index 30684a34b1f3..a5d1f3d493d6 100644 --- a/chia/consensus/constants.py +++ b/chia/consensus/constants.py @@ -56,6 +56,7 @@ class ConsensusConstants: NETWORK_TYPE: int MAX_GENERATOR_SIZE: uint32 MAX_GENERATOR_REF_LIST_SIZE: uint32 + POOL_SUB_SLOT_ITERS: uint64 def replace(self, **changes) -> "ConsensusConstants": return dataclasses.replace(self, **changes) diff --git a/chia/consensus/default_constants.py b/chia/consensus/default_constants.py index f038c21dac71..023c40d025be 100644 --- a/chia/consensus/default_constants.py +++ b/chia/consensus/default_constants.py @@ -54,6 +54,7 @@ testnet_kwargs = { "NETWORK_TYPE": 0, "MAX_GENERATOR_SIZE": 1000000, "MAX_GENERATOR_REF_LIST_SIZE": 512, # Number of references allowed in the block generator ref list + "POOL_SUB_SLOT_ITERS": 37600000000, # iters limit * NUM_SPS } diff --git a/chia/farmer/farmer.py b/chia/farmer/farmer.py index 62c12f2aa6cc..84b5636e3821 100644 --- a/chia/farmer/farmer.py +++ b/chia/farmer/farmer.py @@ -1,28 +1,55 @@ import asyncio +import json import logging import time from pathlib import Path from typing import Any, Callable, Dict, List, Optional, Tuple +import traceback -from blspy import G1Element +import aiohttp +from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey import chia.server.ws_connection as ws # lgtm [py/import-and-import-from] from chia.consensus.coinbase import create_puzzlehash_for_pk from chia.consensus.constants import ConsensusConstants +from chia.pools.pool_config import PoolWalletConfig, load_pool_config from chia.protocols import farmer_protocol, harvester_protocol +from chia.protocols.pool_protocol import ( + ErrorResponse, + get_current_authentication_token, + GetFarmerResponse, + PoolErrorCode, + PostFarmerPayload, + PostFarmerRequest, + PutFarmerPayload, + PutFarmerRequest, + AuthenticationPayload, +) from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import NodeType, make_msg from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.proof_of_space import ProofOfSpace from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash -from chia.util.config import load_config, save_config -from chia.util.ints import uint32, uint64 +from chia.util.config import load_config, save_config, config_path_for_filename +from chia.util.hash import std_hash +from chia.util.ints import uint8, uint16, uint32, uint64 from chia.util.keychain import Keychain -from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk, master_sk_to_wallet_sk +from chia.wallet.derive_keys import ( + master_sk_to_farmer_sk, + master_sk_to_pool_sk, + master_sk_to_wallet_sk, + find_authentication_sk, + find_owner_sk, +) +from chia.wallet.puzzles.singleton_top_layer import SINGLETON_MOD + +singleton_mod_hash = SINGLETON_MOD.get_tree_hash() log = logging.getLogger(__name__) +UPDATE_POOL_INFO_INTERVAL: int = 3600 +UPDATE_POOL_FARMER_INFO_INTERVAL: int = 300 """ HARVESTER PROTOCOL (FARMER <-> HARVESTER) @@ -57,15 +84,17 @@ class Farmer: self.cache_add_time: Dict[bytes32, uint64] = {} self.cache_clear_task: asyncio.Task + self.update_pool_state_task: asyncio.Task self.constants = consensus_constants self._shut_down = False self.server: Any = None self.keychain = keychain self.state_changed_callback: Optional[Callable] = None self.log = log - all_sks = self.keychain.get_all_private_keys() - self._private_keys = [master_sk_to_farmer_sk(sk) for sk, _ in all_sks] + [ - master_sk_to_pool_sk(sk) for sk, _ in all_sks + self.all_root_sks: List[PrivateKey] = [sk for sk, _ in self.keychain.get_all_private_keys()] + + self._private_keys = [master_sk_to_farmer_sk(sk) for sk in self.all_root_sks] + [ + master_sk_to_pool_sk(sk) for sk in self.all_root_sks ] if len(self.get_public_keys()) == 0: @@ -78,7 +107,7 @@ class Farmer: self.pool_public_keys = [G1Element.from_bytes(bytes.fromhex(pk)) for pk in self.config["pool_public_keys"]] - # This is the pool configuration, which should be moved out to the pool once it exists + # This is the self pooling configuration, which is only used for original self-pooled plots self.pool_target_encoded = pool_config["xch_target_address"] self.pool_target = decode_puzzle_hash(self.pool_target_encoded) self.pool_sks_map: Dict = {} @@ -91,7 +120,19 @@ class Farmer: error_str = "No keys exist. Please run 'chia keys generate' or open the UI." raise RuntimeError(error_str) + # The variables below are for use with an actual pool + + # From p2_singleton_puzzle_hash to pool state dict + self.pool_state: Dict[bytes32, Dict] = {} + + # From public key bytes to PrivateKey + self.authentication_keys: Dict[bytes, PrivateKey] = {} + + # Last time we updated pool_state based on the config file + self.last_config_access_time: uint64 = uint64(0) + async def _start(self): + self.update_pool_state_task = asyncio.create_task(self._periodically_update_pool_state_task()) self.cache_clear_task = asyncio.create_task(self._periodically_clear_cache_and_refresh_task()) def _close(self): @@ -99,6 +140,7 @@ class Farmer: async def _await_closed(self): await self.cache_clear_task + await self.update_pool_state_task def _set_state_changed_callback(self, callback: Callable): self.state_changed_callback = callback @@ -121,10 +163,240 @@ class Farmer: if self.state_changed_callback is not None: self.state_changed_callback(change, data) + def handle_failed_pool_response(self, p2_singleton_puzzle_hash: bytes32, error_message: str): + self.log.error(error_message) + self.pool_state[p2_singleton_puzzle_hash]["pool_errors_24h"].append( + ErrorResponse(uint16(PoolErrorCode.REQUEST_FAILED.value), error_message).to_json_dict() + ) + def on_disconnect(self, connection: ws.WSChiaConnection): self.log.info(f"peer disconnected {connection.get_peer_info()}") self.state_changed("close_connection", {}) + async def _pool_get_pool_info(self, pool_config: PoolWalletConfig) -> Optional[Dict]: + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(f"{pool_config.pool_url}/pool_info") as resp: + if resp.ok: + response: Dict = json.loads(await resp.text()) + self.log.info(f"GET /pool_info response: {response}") + return response + else: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, + f"Error in GET /pool_info {pool_config.pool_url}, {resp.status}", + ) + + except Exception as e: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, f"Exception in GET /pool_info {pool_config.pool_url}, {e}" + ) + + return None + + async def _pool_get_farmer( + self, pool_config: PoolWalletConfig, authentication_token_timeout: uint8, authentication_sk: PrivateKey + ) -> Optional[Dict]: + assert authentication_sk.get_g1() == pool_config.authentication_public_key + authentication_token = get_current_authentication_token(authentication_token_timeout) + message: bytes32 = std_hash( + AuthenticationPayload( + "get_farmer", pool_config.launcher_id, pool_config.target_puzzle_hash, authentication_token + ) + ) + signature: G2Element = AugSchemeMPL.sign(authentication_sk, message) + get_farmer_params = { + "launcher_id": pool_config.launcher_id.hex(), + "authentication_token": authentication_token, + "signature": bytes(signature).hex(), + } + try: + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get(f"{pool_config.pool_url}/farmer", params=get_farmer_params) as resp: + if resp.ok: + response: Dict = json.loads(await resp.text()) + self.log.info(f"GET /farmer response: {response}") + if "error_code" in response: + self.pool_state[pool_config.p2_singleton_puzzle_hash]["pool_errors_24h"].append(response) + return response + else: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, + f"Error in GET /farmer {pool_config.pool_url}, {resp.status}", + ) + except Exception as e: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, f"Exception in GET /farmer {pool_config.pool_url}, {e}" + ) + return None + + async def _pool_post_farmer( + self, pool_config: PoolWalletConfig, authentication_token_timeout: uint8, owner_sk: PrivateKey + ) -> Optional[Dict]: + post_farmer_payload: PostFarmerPayload = PostFarmerPayload( + pool_config.launcher_id, + get_current_authentication_token(authentication_token_timeout), + pool_config.authentication_public_key, + pool_config.payout_instructions, + None, + ) + assert owner_sk.get_g1() == pool_config.owner_public_key + signature: G2Element = AugSchemeMPL.sign(owner_sk, post_farmer_payload.get_hash()) + post_farmer_request = PostFarmerRequest(post_farmer_payload, signature) + post_farmer_body = json.dumps(post_farmer_request.to_json_dict()) + + headers = { + "content-type": "application/json;", + } + try: + async with aiohttp.ClientSession() as session: + async with session.post( + f"{pool_config.pool_url}/farmer", data=post_farmer_body, headers=headers + ) as resp: + if resp.ok: + response: Dict = json.loads(await resp.text()) + self.log.info(f"POST /farmer response: {response}") + if "error_code" in response: + self.pool_state[pool_config.p2_singleton_puzzle_hash]["pool_errors_24h"].append(response) + return response + else: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, + f"Error in POST /farmer {pool_config.pool_url}, {resp.status}", + ) + except Exception as e: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, f"Exception in POST /farmer {pool_config.pool_url}, {e}" + ) + return None + + async def _pool_put_farmer( + self, pool_config: PoolWalletConfig, authentication_token_timeout: uint8, owner_sk: PrivateKey + ) -> Optional[Dict]: + put_farmer_payload: PutFarmerPayload = PutFarmerPayload( + pool_config.launcher_id, + get_current_authentication_token(authentication_token_timeout), + pool_config.authentication_public_key, + pool_config.payout_instructions, + None, + ) + assert owner_sk.get_g1() == pool_config.owner_public_key + signature: G2Element = AugSchemeMPL.sign(owner_sk, put_farmer_payload.get_hash()) + put_farmer_request = PutFarmerRequest(put_farmer_payload, signature) + put_farmer_body = json.dumps(put_farmer_request.to_json_dict()) + + try: + async with aiohttp.ClientSession() as session: + async with session.put(f"{pool_config.pool_url}/farmer", data=put_farmer_body) as resp: + if resp.ok: + response: Dict = json.loads(await resp.text()) + self.log.info(f"PUT /farmer response: {response}") + if "error_code" in response: + self.pool_state[pool_config.p2_singleton_puzzle_hash]["pool_errors_24h"].append(response) + return response + else: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, + f"Error in PUT /farmer {pool_config.pool_url}, {resp.status}", + ) + except Exception as e: + self.handle_failed_pool_response( + pool_config.p2_singleton_puzzle_hash, f"Exception in PUT /farmer {pool_config.pool_url}, {e}" + ) + return None + + async def update_pool_state(self): + pool_config_list: List[PoolWalletConfig] = load_pool_config(self._root_path) + for pool_config in pool_config_list: + p2_singleton_puzzle_hash = pool_config.p2_singleton_puzzle_hash + + try: + authentication_sk: Optional[PrivateKey] = await find_authentication_sk( + self.all_root_sks, pool_config.authentication_public_key + ) + if authentication_sk is None: + self.log.error(f"Could not find authentication sk for pk: {pool_config.authentication_public_key}") + continue + if p2_singleton_puzzle_hash not in self.pool_state: + self.authentication_keys[bytes(pool_config.authentication_public_key)] = authentication_sk + self.pool_state[p2_singleton_puzzle_hash] = { + "points_found_since_start": 0, + "points_found_24h": [], + "points_acknowledged_since_start": 0, + "points_acknowledged_24h": [], + "next_farmer_update": 0, + "next_pool_info_update": 0, + "current_points": 0, + "current_difficulty": None, + "pool_errors_24h": [], + "authentication_token_timeout": None, + } + self.log.info(f"Added pool: {pool_config}") + pool_state = self.pool_state[p2_singleton_puzzle_hash] + pool_state["pool_config"] = pool_config + + # Skip state update when self pooling + if pool_config.pool_url == "": + continue + + # TODO: Improve error handling below, inform about unexpected failures + if time.time() >= pool_state["next_pool_info_update"]: + # Makes a GET request to the pool to get the updated information + pool_info = await self._pool_get_pool_info(pool_config) + if pool_info is not None and "error_code" not in pool_info: + pool_state["authentication_token_timeout"] = pool_info["authentication_token_timeout"] + pool_state["next_pool_info_update"] = time.time() + UPDATE_POOL_INFO_INTERVAL + # Only update the first time from GET /pool_info, gets updated from GET /farmer later + if pool_state["current_difficulty"] is None: + pool_state["current_difficulty"] = pool_info["minimum_difficulty"] + + if time.time() >= pool_state["next_farmer_update"]: + authentication_token_timeout = pool_state["authentication_token_timeout"] + + async def update_pool_farmer_info() -> Optional[dict]: + # Run a GET /farmer to see if the farmer is already known by the pool + response = await self._pool_get_farmer( + pool_config, authentication_token_timeout, authentication_sk + ) + if response is not None and "error_code" not in response: + farmer_info: GetFarmerResponse = GetFarmerResponse.from_json_dict(response) + pool_state["current_difficulty"] = farmer_info.current_difficulty + pool_state["current_points"] = farmer_info.current_points + pool_state["next_farmer_update"] = time.time() + UPDATE_POOL_FARMER_INFO_INTERVAL + return response + + if authentication_token_timeout is not None: + update_response = await update_pool_farmer_info() + is_error = update_response is not None and "error_code" in update_response + if is_error and update_response["error_code"] == PoolErrorCode.FARMER_NOT_KNOWN.value: + # Make the farmer known on the pool with a POST /farmer + owner_sk = await find_owner_sk(self.all_root_sks, pool_config.owner_public_key) + post_response = await self._pool_post_farmer( + pool_config, authentication_token_timeout, owner_sk + ) + if post_response is not None and "error_code" not in post_response: + self.log.info( + f"Welcome message from {pool_config.pool_url}: " + f"{post_response['welcome_message']}" + ) + # Now we should be able to update the local farmer info + update_response = await update_pool_farmer_info() + if update_response is not None and "error_code" in update_response: + self.log.error( + f"Failed to update farmer info after POST /farmer: " + f"{update_response['error_code']}, " + f"{update_response['error_message']}" + ) + else: + self.log.warning( + f"No pool specific authentication_token_timeout has been set for {p2_singleton_puzzle_hash}" + f", check communication with the pool." + ) + + except Exception as e: + tb = traceback.format_exc() + self.log.error(f"Exception in update_pool_state for {pool_config.pool_url}, {e} {tb}") + def get_public_keys(self): return [child_sk.get_g1() for child_sk in self._private_keys] @@ -168,6 +440,85 @@ class Farmer: config["pool"]["xch_target_address"] = pool_target_encoded save_config(self._root_path, "config.yaml", config) + async def set_payout_instructions(self, launcher_id: bytes32, payout_instructions: str): + for p2_singleton_puzzle_hash, pool_state_dict in self.pool_state.items(): + if launcher_id == pool_state_dict["pool_config"].launcher_id: + config = load_config(self._root_path, "config.yaml") + new_list = [] + for list_element in config["pool"]["pool_list"]: + if bytes.fromhex(list_element["launcher_id"]) == bytes(launcher_id): + list_element["payout_instructions"] = payout_instructions + new_list.append(list_element) + + config["pool"]["pool_list"] = new_list + save_config(self._root_path, "config.yaml", config) + await self.update_pool_state() + return + + self.log.warning(f"Launcher id: {launcher_id} not found") + + async def generate_login_link(self, launcher_id: bytes32) -> Optional[str]: + for pool_state in self.pool_state.values(): + pool_config: PoolWalletConfig = pool_state["pool_config"] + if pool_config.launcher_id == launcher_id: + authentication_sk: Optional[PrivateKey] = await find_authentication_sk( + self.all_root_sks, pool_config.authentication_public_key + ) + if authentication_sk is None: + self.log.error(f"Could not find authentication sk for pk: {pool_config.authentication_public_key}") + continue + assert authentication_sk.get_g1() == pool_config.authentication_public_key + authentication_token_timeout = pool_state["authentication_token_timeout"] + authentication_token = get_current_authentication_token(authentication_token_timeout) + message: bytes32 = std_hash( + AuthenticationPayload( + "get_login", pool_config.launcher_id, pool_config.target_puzzle_hash, authentication_token + ) + ) + signature: G2Element = AugSchemeMPL.sign(authentication_sk, message) + return ( + pool_config.pool_url + + f"/login?launcher_id={launcher_id.hex()}&authentication_token={authentication_token}" + f"&signature={bytes(signature).hex()}" + ) + + return None + + async def get_plots(self) -> Dict: + rpc_response = {} + for connection in self.server.get_connections(): + if connection.connection_type == NodeType.HARVESTER: + peer_host = connection.peer_host + peer_port = connection.peer_port + peer_full = f"{peer_host}:{peer_port}" + response = await connection.request_plots(harvester_protocol.RequestPlots(), timeout=5) + if response is None: + self.log.error( + "Harvester did not respond. You might need to update harvester to the latest version" + ) + continue + if not isinstance(response, harvester_protocol.RespondPlots): + self.log.error(f"Invalid response from harvester: {peer_host}:{peer_port}") + continue + rpc_response[peer_full] = response.to_json_dict() + return rpc_response + + async def _periodically_update_pool_state_task(self): + time_slept: uint64 = uint64(0) + config_path: Path = config_path_for_filename(self._root_path, "config.yaml") + while not self._shut_down: + # Every time the config file changes, read it to check the pool state + stat_info = config_path.stat() + if stat_info.st_mtime > self.last_config_access_time: + self.last_config_access_time = stat_info.st_mtime + await self.update_pool_state() + time_slept = uint64(0) + elif time_slept > 60: + await self.update_pool_state() + time_slept = uint64(0) + time_slept += 1 + await asyncio.sleep(1) + async def _periodically_clear_cache_and_refresh_task(self): time_slept: uint64 = uint64(0) refresh_slept = 0 diff --git a/chia/farmer/farmer_api.py b/chia/farmer/farmer_api.py index d07752374da0..49c17319ef06 100644 --- a/chia/farmer/farmer_api.py +++ b/chia/farmer/farmer_api.py @@ -1,12 +1,21 @@ +import json import time -from typing import Callable, Optional +from typing import Callable, Optional, List, Any, Dict -from blspy import AugSchemeMPL, G2Element +import aiohttp +from blspy import AugSchemeMPL, G2Element, PrivateKey import chia.server.ws_connection as ws from chia.consensus.pot_iterations import calculate_iterations_quality, calculate_sp_interval_iters from chia.farmer.farmer import Farmer from chia.protocols import farmer_protocol, harvester_protocol +from chia.protocols.harvester_protocol import PoolDifficulty +from chia.protocols.pool_protocol import ( + get_current_authentication_token, + PoolErrorCode, + PostPartialRequest, + PostPartialPayload, +) from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import NodeType, make_msg from chia.types.blockchain_format.pool_target import PoolTarget @@ -73,41 +82,171 @@ class FarmerAPI: sp.difficulty, new_proof_of_space.sp_hash, ) - # Double check that the iters are good - assert required_iters < calculate_sp_interval_iters(self.farmer.constants, sp.sub_slot_iters) - # Proceed at getting the signatures for this PoSpace - request = harvester_protocol.RequestSignatures( - new_proof_of_space.plot_identifier, - new_proof_of_space.challenge_hash, - new_proof_of_space.sp_hash, - [sp.challenge_chain_sp, sp.reward_chain_sp], - ) + # If the iters are good enough to make a block, proceed with the block making flow + if required_iters < calculate_sp_interval_iters(self.farmer.constants, sp.sub_slot_iters): + # Proceed at getting the signatures for this PoSpace + request = harvester_protocol.RequestSignatures( + new_proof_of_space.plot_identifier, + new_proof_of_space.challenge_hash, + new_proof_of_space.sp_hash, + [sp.challenge_chain_sp, sp.reward_chain_sp], + ) - if new_proof_of_space.sp_hash not in self.farmer.proofs_of_space: - self.farmer.proofs_of_space[new_proof_of_space.sp_hash] = [ - ( - new_proof_of_space.plot_identifier, - new_proof_of_space.proof, - ) - ] - else: + if new_proof_of_space.sp_hash not in self.farmer.proofs_of_space: + self.farmer.proofs_of_space[new_proof_of_space.sp_hash] = [] self.farmer.proofs_of_space[new_proof_of_space.sp_hash].append( ( new_proof_of_space.plot_identifier, new_proof_of_space.proof, ) ) - self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time())) - self.farmer.quality_str_to_identifiers[computed_quality_string] = ( - new_proof_of_space.plot_identifier, - new_proof_of_space.challenge_hash, - new_proof_of_space.sp_hash, - peer.peer_node_id, - ) - self.farmer.cache_add_time[computed_quality_string] = uint64(int(time.time())) + self.farmer.cache_add_time[new_proof_of_space.sp_hash] = uint64(int(time.time())) + self.farmer.quality_str_to_identifiers[computed_quality_string] = ( + new_proof_of_space.plot_identifier, + new_proof_of_space.challenge_hash, + new_proof_of_space.sp_hash, + peer.peer_node_id, + ) + self.farmer.cache_add_time[computed_quality_string] = uint64(int(time.time())) - return make_msg(ProtocolMessageTypes.request_signatures, request) + await peer.send_message(make_msg(ProtocolMessageTypes.request_signatures, request)) + + p2_singleton_puzzle_hash = new_proof_of_space.proof.pool_contract_puzzle_hash + if p2_singleton_puzzle_hash is not None: + # Otherwise, send the proof of space to the pool + # When we win a block, we also send the partial to the pool + if p2_singleton_puzzle_hash not in self.farmer.pool_state: + self.farmer.log.info(f"Did not find pool info for {p2_singleton_puzzle_hash}") + return + pool_state_dict: Dict = self.farmer.pool_state[p2_singleton_puzzle_hash] + pool_url = pool_state_dict["pool_config"].pool_url + if pool_url == "": + return + + if pool_state_dict["current_difficulty"] is None: + self.farmer.log.warning( + f"No pool specific difficulty has been set for {p2_singleton_puzzle_hash}, " + f"check communication with the pool, skipping this partial to {pool_url}." + ) + return + + required_iters = calculate_iterations_quality( + self.farmer.constants.DIFFICULTY_CONSTANT_FACTOR, + computed_quality_string, + new_proof_of_space.proof.size, + pool_state_dict["current_difficulty"], + new_proof_of_space.sp_hash, + ) + if required_iters >= calculate_sp_interval_iters( + self.farmer.constants, self.farmer.constants.POOL_SUB_SLOT_ITERS + ): + self.farmer.log.info( + f"Proof of space not good enough for pool {pool_url}: {pool_state_dict['current_difficulty']}" + ) + return + + authentication_token_timeout = pool_state_dict["authentication_token_timeout"] + if authentication_token_timeout is None: + self.farmer.log.warning( + f"No pool specific authentication_token_timeout has been set for {p2_singleton_puzzle_hash}" + f", check communication with the pool." + ) + return + + # Submit partial to pool + is_eos = new_proof_of_space.signage_point_index == 0 + + payload = PostPartialPayload( + pool_state_dict["pool_config"].launcher_id, + get_current_authentication_token(authentication_token_timeout), + new_proof_of_space.proof, + new_proof_of_space.sp_hash, + is_eos, + peer.peer_node_id, + ) + + # The plot key is 2/2 so we need the harvester's half of the signature + m_to_sign = payload.get_hash() + request = harvester_protocol.RequestSignatures( + new_proof_of_space.plot_identifier, + new_proof_of_space.challenge_hash, + new_proof_of_space.sp_hash, + [m_to_sign], + ) + response: Any = await peer.request_signatures(request) + if not isinstance(response, harvester_protocol.RespondSignatures): + self.farmer.log.error(f"Invalid response from harvester: {response}") + return + + assert len(response.message_signatures) == 1 + + plot_signature: Optional[G2Element] = None + for sk in self.farmer.get_private_keys(): + pk = sk.get_g1() + if pk == response.farmer_pk: + agg_pk = ProofOfSpace.generate_plot_public_key(response.local_pk, pk, True) + assert agg_pk == new_proof_of_space.proof.plot_public_key + sig_farmer = AugSchemeMPL.sign(sk, m_to_sign, agg_pk) + taproot_sk: PrivateKey = ProofOfSpace.generate_taproot_sk(response.local_pk, pk) + taproot_sig: G2Element = AugSchemeMPL.sign(taproot_sk, m_to_sign, agg_pk) + + plot_signature = AugSchemeMPL.aggregate( + [sig_farmer, response.message_signatures[0][1], taproot_sig] + ) + assert AugSchemeMPL.verify(agg_pk, m_to_sign, plot_signature) + authentication_pk = pool_state_dict["pool_config"].authentication_public_key + if bytes(authentication_pk) is None: + self.farmer.log.error(f"No authentication sk for {authentication_pk}") + return + authentication_sk: PrivateKey = self.farmer.authentication_keys[bytes(authentication_pk)] + authentication_signature = AugSchemeMPL.sign(authentication_sk, m_to_sign) + + assert plot_signature is not None + + agg_sig: G2Element = AugSchemeMPL.aggregate([plot_signature, authentication_signature]) + + post_partial_request: PostPartialRequest = PostPartialRequest(payload, agg_sig) + post_partial_body = json.dumps(post_partial_request.to_json_dict()) + self.farmer.log.info( + f"Submitting partial for {post_partial_request.payload.launcher_id.hex()} to {pool_url}" + ) + pool_state_dict["points_found_since_start"] += pool_state_dict["current_difficulty"] + pool_state_dict["points_found_24h"].append((time.time(), pool_state_dict["current_difficulty"])) + headers = { + "content-type": "application/json;", + } + try: + async with aiohttp.ClientSession() as session: + async with session.post(f"{pool_url}/partial", data=post_partial_body, headers=headers) as resp: + if resp.ok: + pool_response: Dict = json.loads(await resp.text()) + self.farmer.log.info(f"Pool response: {pool_response}") + if "error_code" in pool_response: + self.farmer.log.error( + f"Error in pooling: " + f"{pool_response['error_code'], pool_response['error_message']}" + ) + pool_state_dict["pool_errors_24h"].append(pool_response) + if pool_response["error_code"] == PoolErrorCode.PROOF_NOT_GOOD_ENOUGH.value: + self.farmer.log.error( + "Partial not good enough, forcing pool farmer update to " + "get our current difficulty." + ) + pool_state_dict["next_farmer_update"] = 0 + await self.farmer.update_pool_state() + else: + new_difficulty = pool_response["new_difficulty"] + pool_state_dict["points_acknowledged_since_start"] += new_difficulty + pool_state_dict["points_acknowledged_24h"].append((time.time(), new_difficulty)) + pool_state_dict["current_difficulty"] = new_difficulty + else: + self.farmer.log.error(f"Error sending partial to {pool_url}, {resp.status}") + except Exception as e: + self.farmer.log.error(f"Error connecting to pool: {e}") + return + + return @api_request async def respond_signatures(self, response: harvester_protocol.RespondSignatures): @@ -134,6 +273,7 @@ class FarmerAPI: if plot_identifier == response.plot_identifier: pospace = candidate_pospace assert pospace is not None + include_taproot: bool = pospace.pool_contract_puzzle_hash is not None computed_quality_string = pospace.verify_and_get_quality_string( self.farmer.constants, response.challenge_hash, response.sp_hash @@ -151,15 +291,26 @@ class FarmerAPI: for sk in self.farmer.get_private_keys(): pk = sk.get_g1() if pk == response.farmer_pk: - agg_pk = ProofOfSpace.generate_plot_public_key(response.local_pk, pk) + agg_pk = ProofOfSpace.generate_plot_public_key(response.local_pk, pk, include_taproot) assert agg_pk == pospace.plot_public_key + if include_taproot: + taproot_sk: PrivateKey = ProofOfSpace.generate_taproot_sk(response.local_pk, pk) + taproot_share_cc_sp: G2Element = AugSchemeMPL.sign(taproot_sk, challenge_chain_sp, agg_pk) + taproot_share_rc_sp: G2Element = AugSchemeMPL.sign(taproot_sk, reward_chain_sp, agg_pk) + else: + taproot_share_cc_sp = G2Element() + taproot_share_rc_sp = G2Element() farmer_share_cc_sp = AugSchemeMPL.sign(sk, challenge_chain_sp, agg_pk) - agg_sig_cc_sp = AugSchemeMPL.aggregate([challenge_chain_sp_harv_sig, farmer_share_cc_sp]) + agg_sig_cc_sp = AugSchemeMPL.aggregate( + [challenge_chain_sp_harv_sig, farmer_share_cc_sp, taproot_share_cc_sp] + ) assert AugSchemeMPL.verify(agg_pk, challenge_chain_sp, agg_sig_cc_sp) # This means it passes the sp filter farmer_share_rc_sp = AugSchemeMPL.sign(sk, reward_chain_sp, agg_pk) - agg_sig_rc_sp = AugSchemeMPL.aggregate([reward_chain_sp_harv_sig, farmer_share_rc_sp]) + agg_sig_rc_sp = AugSchemeMPL.aggregate( + [reward_chain_sp_harv_sig, farmer_share_rc_sp, taproot_share_rc_sp] + ) assert AugSchemeMPL.verify(agg_pk, reward_chain_sp, agg_sig_rc_sp) if pospace.pool_public_key is not None: @@ -211,13 +362,30 @@ class FarmerAPI: ) = response.message_signatures[1] pk = sk.get_g1() if pk == response.farmer_pk: - agg_pk = ProofOfSpace.generate_plot_public_key(response.local_pk, pk) + agg_pk = ProofOfSpace.generate_plot_public_key(response.local_pk, pk, include_taproot) assert agg_pk == pospace.plot_public_key + if include_taproot: + taproot_sk = ProofOfSpace.generate_taproot_sk(response.local_pk, pk) + foliage_sig_taproot: G2Element = AugSchemeMPL.sign(taproot_sk, foliage_block_data_hash, agg_pk) + foliage_transaction_block_sig_taproot: G2Element = AugSchemeMPL.sign( + taproot_sk, foliage_transaction_block_hash, agg_pk + ) + else: + foliage_sig_taproot = G2Element() + foliage_transaction_block_sig_taproot = G2Element() + foliage_sig_farmer = AugSchemeMPL.sign(sk, foliage_block_data_hash, agg_pk) foliage_transaction_block_sig_farmer = AugSchemeMPL.sign(sk, foliage_transaction_block_hash, agg_pk) - foliage_agg_sig = AugSchemeMPL.aggregate([foliage_sig_harvester, foliage_sig_farmer]) + + foliage_agg_sig = AugSchemeMPL.aggregate( + [foliage_sig_harvester, foliage_sig_farmer, foliage_sig_taproot] + ) foliage_block_agg_sig = AugSchemeMPL.aggregate( - [foliage_transaction_block_sig_harvester, foliage_transaction_block_sig_farmer] + [ + foliage_transaction_block_sig_harvester, + foliage_transaction_block_sig_farmer, + foliage_transaction_block_sig_taproot, + ] ) assert AugSchemeMPL.verify(agg_pk, foliage_block_data_hash, foliage_agg_sig) assert AugSchemeMPL.verify(agg_pk, foliage_transaction_block_hash, foliage_block_agg_sig) @@ -237,12 +405,33 @@ class FarmerAPI: @api_request async def new_signage_point(self, new_signage_point: farmer_protocol.NewSignagePoint): + pool_difficulties: List[PoolDifficulty] = [] + for p2_singleton_puzzle_hash, pool_dict in self.farmer.pool_state.items(): + if pool_dict["pool_config"].pool_url == "": + # Self pooling + continue + + if pool_dict["current_difficulty"] is None: + self.farmer.log.warning( + f"No pool specific difficulty has been set for {p2_singleton_puzzle_hash}, " + f"check communication with the pool, skipping this signage point, pool: " + f"{pool_dict['pool_config'].pool_url} " + ) + continue + pool_difficulties.append( + PoolDifficulty( + pool_dict["current_difficulty"], + self.farmer.constants.POOL_SUB_SLOT_ITERS, + p2_singleton_puzzle_hash, + ) + ) message = harvester_protocol.NewSignagePointHarvester( new_signage_point.challenge_hash, new_signage_point.difficulty, new_signage_point.sub_slot_iters, new_signage_point.signage_point_index, new_signage_point.challenge_chain_sp, + pool_difficulties, ) msg = make_msg(ProtocolMessageTypes.new_signage_point_harvester, message) @@ -291,3 +480,7 @@ class FarmerAPI: } }, ) + + @api_request + async def respond_plots(self, _: harvester_protocol.RespondPlots): + self.farmer.log.warning("Respond plots came too late") diff --git a/chia/full_node/full_node.py b/chia/full_node/full_node.py index 405de56b8cac..cb975cc26421 100644 --- a/chia/full_node/full_node.py +++ b/chia/full_node/full_node.py @@ -101,6 +101,7 @@ class FullNode: self.sync_store = None self.signage_point_times = [time.time() for _ in range(self.constants.NUM_SPS_SUB_SLOT)] self.full_node_store = FullNodeStore(self.constants) + self.uncompact_task = None self.log = logging.getLogger(name if name else __name__) @@ -148,7 +149,6 @@ class FullNode: assert len(pending_tx) == 0 # no pending transactions when starting up peak: Optional[BlockRecord] = self.blockchain.get_peak() - self.uncompact_task = None if peak is not None: full_peak = await self.blockchain.get_full_peak() await self.peak_post_processing(full_peak, peak, max(peak.height - 1, 0), None) diff --git a/chia/full_node/full_node_store.py b/chia/full_node/full_node_store.py index e15bc142e579..2315946b8522 100644 --- a/chia/full_node/full_node_store.py +++ b/chia/full_node/full_node_store.py @@ -23,6 +23,7 @@ from chia.types.full_block import FullBlock from chia.types.generator_types import CompressorArg from chia.types.unfinished_block import UnfinishedBlock from chia.util.ints import uint8, uint32, uint64, uint128 +from chia.util.lru_cache import LRUCache log = logging.getLogger(__name__) @@ -61,6 +62,10 @@ class FullNodeStore: # This stores the time that each key was added to the future cache, so we can clear old keys future_cache_key_times: Dict[bytes32, int] + # These recent caches are for pooling support + recent_signage_points: LRUCache + recent_eos: LRUCache + # Partial hashes of unfinished blocks we are requesting requesting_unfinished_blocks: Set[bytes32] @@ -80,6 +85,8 @@ class FullNodeStore: self.future_eos_cache = {} self.future_sp_cache = {} self.future_ip_cache = {} + self.recent_signage_points = LRUCache(500) + self.recent_eos = LRUCache(50) self.requesting_unfinished_blocks = set() self.previous_generator = None self.future_cache_key_times = {} @@ -426,6 +433,9 @@ class FullNodeStore: self.finished_sub_slots.append((eos, [None] * self.constants.NUM_SPS_SUB_SLOT, total_iters)) + new_cc_hash = eos.challenge_chain.get_hash() + self.recent_eos.put(new_cc_hash, (eos, time.time())) + new_ips: List[timelord_protocol.NewInfusionPointVDF] = [] for ip in self.future_ip_cache.get(eos.reward_chain.get_hash(), []): new_ips.append(ip) @@ -566,6 +576,7 @@ class FullNodeStore: return False sp_arr[index] = signage_point + self.recent_signage_points.put(signage_point.cc_vdf.output.get_hash(), (signage_point, time.time())) return True self.add_to_future_sp(signage_point, index) return False @@ -728,6 +739,10 @@ class FullNodeStore: self.future_sp_cache.pop(peak.reward_infusion_new_challenge, []) self.future_ip_cache.pop(peak.reward_infusion_new_challenge, []) + for eos_op, _, _ in self.finished_sub_slots: + if eos_op is not None: + self.recent_eos.put(eos_op.challenge_chain.get_hash(), (eos_op, time.time())) + return new_eos, new_sps, new_ips def get_finished_sub_slots( diff --git a/chia/full_node/generator.py b/chia/full_node/generator.py index 82d4b15cd7cd..1d37db68ca3e 100644 --- a/chia/full_node/generator.py +++ b/chia/full_node/generator.py @@ -21,7 +21,7 @@ log = logging.getLogger(__name__) def create_block_generator( generator: SerializedProgram, block_heights_list: List[uint32], generator_block_cache: GeneratorBlockCacheInterface ) -> Optional[BlockGenerator]: - """ `create_block_generator` will returns None if it fails to look up any referenced block """ + """`create_block_generator` will returns None if it fails to look up any referenced block""" generator_arg_list: List[GeneratorArg] = [] for i in block_heights_list: previous_generator = generator_block_cache.get_generator_for_block_height(i) diff --git a/chia/full_node/mempool_check_conditions.py b/chia/full_node/mempool_check_conditions.py index 46850932516a..b10afae7301a 100644 --- a/chia/full_node/mempool_check_conditions.py +++ b/chia/full_node/mempool_check_conditions.py @@ -1,3 +1,4 @@ +import logging import time from typing import Tuple, Dict, List, Optional, Set from clvm import SExp @@ -27,17 +28,22 @@ def mempool_assert_announcement(condition: ConditionWithArgs, announcements: Set Check if an announcement is included in the list of announcements """ announcement_hash = bytes32(condition.vars[0]) + if announcement_hash not in announcements: return Err.ASSERT_ANNOUNCE_CONSUMED_FAILED return None +log = logging.getLogger(__name__) + + def mempool_assert_my_coin_id(condition: ConditionWithArgs, unspent: CoinRecord) -> Optional[Err]: """ Checks if CoinID matches the id from the condition """ if unspent.coin.name() != condition.vars[0]: + log.warning(f"My name: {unspent.coin.name()} got: {condition.vars[0].hex()}") return Err.ASSERT_MY_COIN_ID_FAILED return None diff --git a/chia/full_node/mempool_manager.py b/chia/full_node/mempool_manager.py index 810f6278f607..e531e80f6b4d 100644 --- a/chia/full_node/mempool_manager.py +++ b/chia/full_node/mempool_manager.py @@ -300,6 +300,11 @@ class MempoolManager: for name in removal_names: removal_record = await self.coin_store.get_coin_record(name) if removal_record is None and name not in additions_dict: + log.error( + "MempoolInclusionStatus.FAILED, Err.UNKNOWN_UNSPENT:\n" + f"COIN ID: {name}\nNPC RESULT: {npc_result}\nSPEND: {new_spend}" + ) + new_spend.debug() return None, MempoolInclusionStatus.FAILED, Err.UNKNOWN_UNSPENT elif name in additions_dict: removal_coin = additions_dict[name] diff --git a/chia/harvester/harvester.py b/chia/harvester/harvester.py index ed1c00d02cc3..486bc1608e4c 100644 --- a/chia/harvester/harvester.py +++ b/chia/harvester/harvester.py @@ -84,7 +84,8 @@ class Harvester: { "filename": str(path), "size": prover.get_size(), - "plot-seed": prover.get_id(), + "plot-seed": prover.get_id(), # Deprecated + "plot_id": prover.get_id(), "pool_public_key": plot_info.pool_public_key, "pool_contract_puzzle_hash": plot_info.pool_contract_puzzle_hash, "plot_public_key": plot_info.plot_public_key, diff --git a/chia/harvester/harvester_api.py b/chia/harvester/harvester_api.py index 3abe18dcb9f2..65c511882598 100644 --- a/chia/harvester/harvester_api.py +++ b/chia/harvester/harvester_api.py @@ -3,13 +3,14 @@ import time from pathlib import Path from typing import Callable, List, Tuple -from blspy import AugSchemeMPL, G2Element +from blspy import AugSchemeMPL, G2Element, G1Element from chia.consensus.pot_iterations import calculate_iterations_quality, calculate_sp_interval_iters from chia.harvester.harvester import Harvester from chia.plotting.plot_tools import PlotInfo, parse_plot_info from chia.protocols import harvester_protocol from chia.protocols.farmer_protocol import FarmingInfo +from chia.protocols.harvester_protocol import Plot from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import make_msg from chia.server.ws_connection import WSChiaConnection @@ -98,18 +99,27 @@ class HarvesterAPI: responses: List[Tuple[bytes32, ProofOfSpace]] = [] if quality_strings is not None: + difficulty = new_challenge.difficulty + sub_slot_iters = new_challenge.sub_slot_iters + if plot_info.pool_contract_puzzle_hash is not None: + # If we are pooling, override the difficulty and sub slot iters with the pool threshold info. + # This will mean more proofs actually get found, but they are only submitted to the pool, + # not the blockchain + for pool_difficulty in new_challenge.pool_difficulties: + if pool_difficulty.pool_contract_puzzle_hash == plot_info.pool_contract_puzzle_hash: + difficulty = pool_difficulty.difficulty + sub_slot_iters = pool_difficulty.sub_slot_iters + # Found proofs of space (on average 1 is expected per plot) for index, quality_str in enumerate(quality_strings): required_iters: uint64 = calculate_iterations_quality( self.harvester.constants.DIFFICULTY_CONSTANT_FACTOR, quality_str, plot_info.prover.get_size(), - new_challenge.difficulty, + difficulty, new_challenge.sp_hash, ) - sp_interval_iters = calculate_sp_interval_iters( - self.harvester.constants, new_challenge.sub_slot_iters - ) + sp_interval_iters = calculate_sp_interval_iters(self.harvester.constants, sub_slot_iters) if required_iters < sp_interval_iters: # Found a very good proof of space! will fetch the whole proof from disk, # then send to farmer @@ -130,8 +140,9 @@ class HarvesterAPI: local_master_sk, ) = parse_plot_info(plot_info.prover.get_memo()) local_sk = master_sk_to_local_sk(local_master_sk) + include_taproot = plot_info.pool_contract_puzzle_hash is not None plot_public_key = ProofOfSpace.generate_plot_public_key( - local_sk.get_g1(), farmer_public_key + local_sk.get_g1(), farmer_public_key, include_taproot ) responses.append( ( @@ -251,7 +262,13 @@ class HarvesterAPI: ) = parse_plot_info(plot_info.prover.get_memo()) local_sk = master_sk_to_local_sk(local_master_sk) - agg_pk = ProofOfSpace.generate_plot_public_key(local_sk.get_g1(), farmer_public_key) + if isinstance(pool_public_key_or_puzzle_hash, G1Element): + include_taproot = False + else: + assert isinstance(pool_public_key_or_puzzle_hash, bytes32) + include_taproot = True + + agg_pk = ProofOfSpace.generate_plot_public_key(local_sk.get_g1(), farmer_public_key, include_taproot) # This is only a partial signature. When combined with the farmer's half, it will # form a complete PrependSignature. @@ -270,3 +287,24 @@ class HarvesterAPI: ) return make_msg(ProtocolMessageTypes.respond_signatures, response) + + @api_request + async def request_plots(self, _: harvester_protocol.RequestPlots): + plots_response = [] + plots, failed_to_open_filenames, no_key_filenames = self.harvester.get_plots() + for plot in plots: + plots_response.append( + Plot( + plot["filename"], + plot["size"], + plot["plot_id"], + plot["pool_public_key"], + plot["pool_contract_puzzle_hash"], + plot["plot_public_key"], + plot["file_size"], + plot["time_modified"], + ) + ) + + response = harvester_protocol.RespondPlots(plots_response, failed_to_open_filenames, no_key_filenames) + return make_msg(ProtocolMessageTypes.respond_plots, response) diff --git a/chia/plotting/create_plots.py b/chia/plotting/create_plots.py index 1f2cc2a2cb87..6c3ebe374cb5 100644 --- a/chia/plotting/create_plots.py +++ b/chia/plotting/create_plots.py @@ -113,7 +113,11 @@ def create_plots(args, root_path, use_datetime=True, test_private_keys: Optional sk = AugSchemeMPL.key_gen(token_bytes(32)) # The plot public key is the combination of the harvester and farmer keys - plot_public_key = ProofOfSpace.generate_plot_public_key(master_sk_to_local_sk(sk).get_g1(), farmer_public_key) + # New plots will also include a taproot of the keys, for extensibility + include_taproot: bool = pool_contract_puzzle_hash is not None + plot_public_key = ProofOfSpace.generate_plot_public_key( + master_sk_to_local_sk(sk).get_g1(), farmer_public_key, include_taproot + ) # The plot id is based on the harvester, farmer, and pool keys if pool_public_key is not None: diff --git a/chia/plotting/plot_tools.py b/chia/plotting/plot_tools.py index 5683c3a9f1ea..5a7a903dbda2 100644 --- a/chia/plotting/plot_tools.py +++ b/chia/plotting/plot_tools.py @@ -234,7 +234,10 @@ def load_plots( stat_info = filename.stat() local_sk = master_sk_to_local_sk(local_master_sk) - plot_public_key: G1Element = ProofOfSpace.generate_plot_public_key(local_sk.get_g1(), farmer_public_key) + + plot_public_key: G1Element = ProofOfSpace.generate_plot_public_key( + local_sk.get_g1(), farmer_public_key, pool_contract_puzzle_hash is not None + ) with plot_ids_lock: if prover.get_id() in plot_ids: diff --git a/chia/pools/__init__.py b/chia/pools/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/chia/pools/pool_config.py b/chia/pools/pool_config.py new file mode 100644 index 000000000000..75675ca1fd2e --- /dev/null +++ b/chia/pools/pool_config.py @@ -0,0 +1,66 @@ +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import List + +from blspy import G1Element + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.byte_types import hexstr_to_bytes +from chia.util.config import load_config, save_config +from chia.util.streamable import Streamable, streamable + +""" +Config example +This is what goes into the user's config file, to communicate between the wallet and the farmer processes. +pool_list: + launcher_id: ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa + authentication_public_key: 970e181ae45435ae696508a78012dc80548c334cf29676ea6ade7049eb9d2b9579cc30cb44c3fd68d35a250cfbc69e29 + owner_public_key: 84c3fcf9d5581c1ddc702cb0f3b4a06043303b334dd993ab42b2c320ebfa98e5ce558448615b3f69638ba92cf7f43da5 + payout_instructions: c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8 + pool_url: localhost + p2_singleton_puzzle_hash: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 + target_puzzle_hash: 344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58 +""" # noqa + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +@streamable +class PoolWalletConfig(Streamable): + launcher_id: bytes32 + pool_url: str + payout_instructions: str + target_puzzle_hash: bytes32 + p2_singleton_puzzle_hash: bytes32 + owner_public_key: G1Element + authentication_public_key: G1Element + + +def load_pool_config(root_path: Path) -> List[PoolWalletConfig]: + config = load_config(root_path, "config.yaml") + ret_list: List[PoolWalletConfig] = [] + if "pool_list" in config["pool"]: + for pool_config_dict in config["pool"]["pool_list"]: + try: + pool_config = PoolWalletConfig( + hexstr_to_bytes(pool_config_dict["launcher_id"]), + pool_config_dict["pool_url"], + pool_config_dict["payout_instructions"], + hexstr_to_bytes(pool_config_dict["target_puzzle_hash"]), + hexstr_to_bytes(pool_config_dict["p2_singleton_puzzle_hash"]), + G1Element.from_bytes(hexstr_to_bytes(pool_config_dict["owner_public_key"])), + G1Element.from_bytes(hexstr_to_bytes(pool_config_dict["authentication_public_key"])), + ) + ret_list.append(pool_config) + except Exception as e: + log.error(f"Exception loading config: {pool_config_dict} {e}") + + return ret_list + + +async def update_pool_config(root_path: Path, pool_config_list: List[PoolWalletConfig]): + full_config = load_config(root_path, "config.yaml") + full_config["pool"]["pool_list"] = [c.to_json_dict() for c in pool_config_list] + save_config(root_path, "config.yaml", full_config) diff --git a/chia/pools/pool_puzzles.py b/chia/pools/pool_puzzles.py new file mode 100644 index 000000000000..6772c54b993c --- /dev/null +++ b/chia/pools/pool_puzzles.py @@ -0,0 +1,430 @@ +import logging +from typing import Tuple, List, Optional +from blspy import G1Element +from clvm.casts import int_from_bytes, int_to_bytes + +from chia.clvm.singleton import SINGLETON_LAUNCHER +from chia.consensus.block_rewards import calculate_pool_reward +from chia.consensus.coinbase import pool_parent_id +from chia.pools.pool_wallet_info import PoolState, LEAVING_POOL, SELF_POOLING + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program, SerializedProgram + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_solution import CoinSolution +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.puzzles.singleton_top_layer import puzzle_for_singleton + +from chia.util.ints import uint32, uint64 + +log = logging.getLogger(__name__) +# "Full" is the outer singleton, with the inner puzzle filled in +SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") +POOL_WAITING_ROOM_MOD = load_clvm("pool_waitingroom_innerpuz.clvm") +POOL_MEMBER_MOD = load_clvm("pool_member_innerpuz.clvm") +P2_SINGLETON_MOD = load_clvm("p2_singleton_or_delayed_puzhash.clvm") +POOL_OUTER_MOD = SINGLETON_MOD + +POOL_MEMBER_HASH = POOL_MEMBER_MOD.get_tree_hash() +POOL_WAITING_ROOM_HASH = POOL_WAITING_ROOM_MOD.get_tree_hash() +P2_SINGLETON_HASH = P2_SINGLETON_MOD.get_tree_hash() +POOL_OUTER_MOD_HASH = POOL_OUTER_MOD.get_tree_hash() +SINGLETON_LAUNCHER_HASH = SINGLETON_LAUNCHER.get_tree_hash() +SINGLETON_MOD_HASH = POOL_OUTER_MOD_HASH + +SINGLETON_MOD_HASH_HASH = Program.to(SINGLETON_MOD_HASH).get_tree_hash() + + +def create_waiting_room_inner_puzzle( + target_puzzle_hash: bytes32, + relative_lock_height: uint32, + owner_pubkey: G1Element, + launcher_id: bytes32, + genesis_challenge: bytes32, + delay_time: uint64, + delay_ph: bytes32, +) -> Program: + pool_reward_prefix = bytes32(genesis_challenge[:16] + b"\x00" * 16) + p2_singleton_puzzle_hash: bytes32 = launcher_id_to_p2_puzzle_hash(launcher_id, delay_time, delay_ph) + return POOL_WAITING_ROOM_MOD.curry( + target_puzzle_hash, p2_singleton_puzzle_hash, bytes(owner_pubkey), pool_reward_prefix, relative_lock_height + ) + + +def create_pooling_inner_puzzle( + target_puzzle_hash: bytes, + pool_waiting_room_inner_hash: bytes32, + owner_pubkey: G1Element, + launcher_id: bytes32, + genesis_challenge: bytes32, + delay_time: uint64, + delay_ph: bytes32, +) -> Program: + pool_reward_prefix = bytes32(genesis_challenge[:16] + b"\x00" * 16) + p2_singleton_puzzle_hash: bytes32 = launcher_id_to_p2_puzzle_hash(launcher_id, delay_time, delay_ph) + return POOL_MEMBER_MOD.curry( + target_puzzle_hash, + p2_singleton_puzzle_hash, + bytes(owner_pubkey), + pool_reward_prefix, + pool_waiting_room_inner_hash, + ) + + +def create_full_puzzle(inner_puzzle: Program, launcher_id: bytes32) -> Program: + return puzzle_for_singleton(launcher_id, inner_puzzle) + + +def create_p2_singleton_puzzle( + singleton_mod_hash: bytes, + launcher_id: bytes32, + seconds_delay: uint64, + delayed_puzzle_hash: bytes32, +) -> Program: + # curry params are SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH SECONDS_DELAY DELAYED_PUZZLE_HASH + return P2_SINGLETON_MOD.curry( + singleton_mod_hash, launcher_id, SINGLETON_LAUNCHER_HASH, seconds_delay, delayed_puzzle_hash + ) + + +def launcher_id_to_p2_puzzle_hash(launcher_id: bytes32, seconds_delay: uint64, delayed_puzzle_hash: bytes32) -> bytes32: + return create_p2_singleton_puzzle( + SINGLETON_MOD_HASH, launcher_id, int_to_bytes(seconds_delay), delayed_puzzle_hash + ).get_tree_hash() + + +def get_delayed_puz_info_from_launcher_spend(coinsol: CoinSolution) -> Tuple[uint64, bytes32]: + extra_data = Program.from_bytes(bytes(coinsol.solution)).rest().rest().first() + # Extra data is (pool_state delayed_puz_info) + # Delayed puz info is (seconds delayed_puzzle_hash) + seconds: Optional[uint64] = None + delayed_puzzle_hash: Optional[bytes32] = None + for key, value in extra_data.as_python(): + if key == b"t": + seconds = int_from_bytes(value) + if key == b"h": + delayed_puzzle_hash = bytes32(value) + assert seconds is not None + assert delayed_puzzle_hash is not None + return seconds, delayed_puzzle_hash + + +###################################### + + +def get_template_singleton_inner_puzzle(inner_puzzle: Program): + r = inner_puzzle.uncurry() + if r is None: + return False + uncurried_inner_puzzle, args = r + return uncurried_inner_puzzle + + +def get_seconds_and_delayed_puzhash_from_p2_singleton_puzzle(puzzle: Program) -> Tuple[uint64, bytes32]: + r = puzzle.uncurry() + if r is None: + return False + inner_f, args = r + singleton_mod_hash, launcher_id, launcher_puzzle_hash, seconds_delay, delayed_puzzle_hash = list(args.as_iter()) + seconds_delay = uint64(seconds_delay.as_int()) + return seconds_delay, delayed_puzzle_hash.as_atom() + + +# Verify that a puzzle is a Pool Wallet Singleton +def is_pool_singleton_inner_puzzle(inner_puzzle: Program) -> bool: + inner_f = get_template_singleton_inner_puzzle(inner_puzzle) + return inner_f in [POOL_WAITING_ROOM_MOD, POOL_MEMBER_MOD] + + +def is_pool_waitingroom_inner_puzzle(inner_puzzle: Program) -> bool: + inner_f = get_template_singleton_inner_puzzle(inner_puzzle) + return inner_f in [POOL_WAITING_ROOM_MOD] + + +def is_pool_member_inner_puzzle(inner_puzzle: Program) -> bool: + inner_f = get_template_singleton_inner_puzzle(inner_puzzle) + return inner_f in [POOL_MEMBER_MOD] + + +# This spend will use the escape-type spend path for whichever state you are currently in +# If you are currently a waiting inner puzzle, then it will look at your target_state to determine the next +# inner puzzle hash to go to. The member inner puzzle is already committed to its next puzzle hash. +def create_travel_spend( + last_coin_solution: CoinSolution, + launcher_coin: Coin, + current: PoolState, + target: PoolState, + genesis_challenge: bytes32, + delay_time: uint64, + delay_ph: bytes32, +) -> Tuple[CoinSolution, Program]: + inner_puzzle: Program = pool_state_to_inner_puzzle( + current, + launcher_coin.name(), + genesis_challenge, + delay_time, + delay_ph, + ) + if is_pool_member_inner_puzzle(inner_puzzle): + # inner sol is key_value_list () + # key_value_list is: + # "ps" -> poolstate as bytes + inner_sol: Program = Program.to([[("p", bytes(target))], 0]) + elif is_pool_waitingroom_inner_puzzle(inner_puzzle): + # inner sol is (spend_type, key_value_list, pool_reward_height) + destination_inner: Program = pool_state_to_inner_puzzle( + target, launcher_coin.name(), genesis_challenge, delay_time, delay_ph + ) + log.warning( + f"create_travel_spend: waitingroom: target PoolState bytes:\n{bytes(target).hex()}\n" + f"{target}" + f"hash:{Program.to(bytes(target)).get_tree_hash()}" + ) + # key_value_list is: + # "ps" -> poolstate as bytes + inner_sol = Program.to([1, [("p", bytes(target))], destination_inner.get_tree_hash()]) # current or target + else: + raise ValueError + + current_singleton: Optional[Coin] = get_most_recent_singleton_coin_from_coin_solution(last_coin_solution) + assert current_singleton is not None + + if current_singleton.parent_coin_info == launcher_coin.name(): + parent_info_list = Program.to([launcher_coin.parent_coin_info, launcher_coin.amount]) + else: + p = Program.from_bytes(bytes(last_coin_solution.puzzle_reveal)) + last_coin_solution_inner_puzzle: Optional[Program] = get_inner_puzzle_from_puzzle(p) + assert last_coin_solution_inner_puzzle is not None + parent_info_list = Program.to( + [ + last_coin_solution.coin.parent_coin_info, + last_coin_solution_inner_puzzle.get_tree_hash(), + last_coin_solution.coin.amount, + ] + ) + full_solution: Program = Program.to([parent_info_list, current_singleton.amount, inner_sol]) + full_puzzle: Program = create_full_puzzle(inner_puzzle, launcher_coin.name()) + + return ( + CoinSolution( + current_singleton, + SerializedProgram.from_program(full_puzzle), + SerializedProgram.from_program(full_solution), + ), + inner_puzzle, + ) + + +def create_absorb_spend( + last_coin_solution: CoinSolution, + current_state: PoolState, + launcher_coin: Coin, + height: uint32, + genesis_challenge: bytes32, + delay_time: uint64, + delay_ph: bytes32, +) -> List[CoinSolution]: + inner_puzzle: Program = pool_state_to_inner_puzzle( + current_state, launcher_coin.name(), genesis_challenge, delay_time, delay_ph + ) + reward_amount: uint64 = calculate_pool_reward(height) + if is_pool_member_inner_puzzle(inner_puzzle): + # inner sol is (spend_type, pool_reward_amount, pool_reward_height, extra_data) + inner_sol: Program = Program.to([reward_amount, height]) + elif is_pool_waitingroom_inner_puzzle(inner_puzzle): + # inner sol is (spend_type, destination_puzhash, pool_reward_amount, pool_reward_height, extra_data) + inner_sol = Program.to([0, reward_amount, height]) + else: + raise ValueError + # full sol = (parent_info, my_amount, inner_solution) + coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_solution(last_coin_solution) + assert coin is not None + + if coin.parent_coin_info == launcher_coin.name(): + parent_info: Program = Program.to([launcher_coin.parent_coin_info, launcher_coin.amount]) + else: + p = Program.from_bytes(bytes(last_coin_solution.puzzle_reveal)) + last_coin_solution_inner_puzzle: Optional[Program] = get_inner_puzzle_from_puzzle(p) + assert last_coin_solution_inner_puzzle is not None + parent_info = Program.to( + [ + last_coin_solution.coin.parent_coin_info, + last_coin_solution_inner_puzzle.get_tree_hash(), + last_coin_solution.coin.amount, + ] + ) + full_solution: SerializedProgram = SerializedProgram.from_program( + Program.to([parent_info, last_coin_solution.coin.amount, inner_sol]) + ) + full_puzzle: SerializedProgram = SerializedProgram.from_program( + create_full_puzzle(inner_puzzle, launcher_coin.name()) + ) + assert coin.puzzle_hash == full_puzzle.get_tree_hash() + + reward_parent: bytes32 = pool_parent_id(height, genesis_challenge) + p2_singleton_puzzle: SerializedProgram = SerializedProgram.from_program( + create_p2_singleton_puzzle(SINGLETON_MOD_HASH, launcher_coin.name(), delay_time, delay_ph) + ) + reward_coin: Coin = Coin(reward_parent, p2_singleton_puzzle.get_tree_hash(), reward_amount) + p2_singleton_solution: SerializedProgram = SerializedProgram.from_program( + Program.to([inner_puzzle.get_tree_hash(), reward_coin.name()]) + ) + assert p2_singleton_puzzle.get_tree_hash() == reward_coin.puzzle_hash + assert full_puzzle.get_tree_hash() == coin.puzzle_hash + assert get_inner_puzzle_from_puzzle(Program.from_bytes(bytes(full_puzzle))) is not None + + coin_solutions = [ + CoinSolution(coin, full_puzzle, full_solution), + CoinSolution(reward_coin, p2_singleton_puzzle, p2_singleton_solution), + ] + return coin_solutions + + +def get_most_recent_singleton_coin_from_coin_solution(coin_sol: CoinSolution) -> Optional[Coin]: + additions: List[Coin] = coin_sol.additions() + for coin in additions: + if coin.amount % 2 == 1: + return coin + return None + + +def get_pubkey_from_member_inner_puzzle(inner_puzzle: Program) -> G1Element: + args = uncurry_pool_member_inner_puzzle(inner_puzzle) + if args is not None: + ( + _inner_f, + _target_puzzle_hash, + _p2_singleton_hash, + pubkey_program, + _pool_reward_prefix, + _escape_puzzlehash, + ) = args + else: + raise ValueError("Unable to extract pubkey") + pubkey = G1Element.from_bytes(pubkey_program.as_atom()) + return pubkey + + +def uncurry_pool_member_inner_puzzle(inner_puzzle: Program): # -> Optional[Tuple[Program, Program, Program]]: + """ + Take a puzzle and return `None` if it's not a "pool member" inner puzzle, or + a triple of `mod_hash, relative_lock_height, pubkey` if it is. + """ + if not is_pool_member_inner_puzzle(inner_puzzle): + raise ValueError("Attempting to unpack a non-waitingroom inner puzzle") + r = inner_puzzle.uncurry() + if r is None: + raise ValueError("Failed to unpack inner puzzle") + inner_f, args = r + # p2_singleton_hash is the tree hash of the unique, curried P2_SINGLETON_MOD. See `create_p2_singleton_puzzle` + # escape_puzzlehash is of the unique, curried POOL_WAITING_ROOM_MOD. See `create_waiting_room_inner_puzzle` + target_puzzle_hash, p2_singleton_hash, owner_pubkey, pool_reward_prefix, escape_puzzlehash = tuple(args.as_iter()) + return inner_f, target_puzzle_hash, p2_singleton_hash, owner_pubkey, pool_reward_prefix, escape_puzzlehash + + +def uncurry_pool_waitingroom_inner_puzzle(inner_puzzle: Program) -> Tuple[Program, Program, Program, Program]: + """ + Take a puzzle and return `None` if it's not a "pool member" inner puzzle, or + a triple of `mod_hash, relative_lock_height, pubkey` if it is. + """ + if not is_pool_waitingroom_inner_puzzle(inner_puzzle): + raise ValueError("Attempting to unpack a non-waitingroom inner puzzle") + r = inner_puzzle.uncurry() + if r is None: + raise ValueError("Failed to unpack inner puzzle") + inner_f, args = r + v = args.as_iter() + target_puzzle_hash, p2_singleton_hash, owner_pubkey, genesis_challenge, relative_lock_height = tuple(v) + return target_puzzle_hash, relative_lock_height, owner_pubkey, p2_singleton_hash + + +def get_inner_puzzle_from_puzzle(full_puzzle: Program) -> Optional[Program]: + p = Program.from_bytes(bytes(full_puzzle)) + r = p.uncurry() + if r is None: + return None + _, args = r + + _, inner_puzzle = list(args.as_iter()) + if not is_pool_singleton_inner_puzzle(inner_puzzle): + return None + return inner_puzzle + + +def pool_state_from_extra_data(extra_data: Program) -> Optional[PoolState]: + state_bytes: Optional[bytes] = None + try: + for key, value in extra_data.as_python(): + if key == b"p": + state_bytes = value + break + if state_bytes is None: + return None + return PoolState.from_bytes(state_bytes) + except TypeError as e: + log.error(f"Unexpected return from PoolWallet Smart Contract code {e}") + return None + + +def solution_to_extra_data(full_spend: CoinSolution) -> Optional[PoolState]: + full_solution_ser: SerializedProgram = full_spend.solution + full_solution: Program = Program.from_bytes(bytes(full_solution_ser)) + + if full_spend.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH: + # Launcher spend + extra_data: Program = full_solution.rest().rest().first() + return pool_state_from_extra_data(extra_data) + + # Not launcher spend + inner_solution: Program = full_solution.rest().rest().first() + + # Spend which is not absorb, and is not the launcher + num_args = len(inner_solution.as_python()) + assert num_args in (2, 3) + + if num_args == 2: + # pool member + if inner_solution.rest().first().as_int() != 0: + return None + + # This is referred to as p1 in the chialisp code + # spend_type is absorbing money if p1 is a cons box, spend_type is escape if p1 is an atom + # TODO: The comment above, and in the CLVM, seems wrong + extra_data = inner_solution.first() + if isinstance(extra_data.as_python(), bytes): + # Absorbing + return None + return pool_state_from_extra_data(extra_data) + else: + # pool waitingroom + if inner_solution.first().as_int() == 0: + return None + extra_data = inner_solution.rest().first() + return pool_state_from_extra_data(extra_data) + + +def pool_state_to_inner_puzzle( + pool_state: PoolState, launcher_id: bytes32, genesis_challenge: bytes32, delay_time: uint64, delay_ph: bytes32 +) -> Program: + escaping_inner_puzzle: Program = create_waiting_room_inner_puzzle( + pool_state.target_puzzle_hash, + pool_state.relative_lock_height, + pool_state.owner_pubkey, + launcher_id, + genesis_challenge, + delay_time, + delay_ph, + ) + if pool_state.state in [LEAVING_POOL, SELF_POOLING]: + return escaping_inner_puzzle + else: + return create_pooling_inner_puzzle( + pool_state.target_puzzle_hash, + escaping_inner_puzzle.get_tree_hash(), + pool_state.owner_pubkey, + launcher_id, + genesis_challenge, + delay_time, + delay_ph, + ) diff --git a/chia/pools/pool_wallet.py b/chia/pools/pool_wallet.py new file mode 100644 index 000000000000..a8da39544842 --- /dev/null +++ b/chia/pools/pool_wallet.py @@ -0,0 +1,869 @@ +import logging +import time +from typing import Any, Optional, Set, Tuple, List, Dict + +from blspy import PrivateKey, G2Element, G1Element + +from chia.consensus.block_record import BlockRecord +from chia.pools.pool_config import PoolWalletConfig, load_pool_config, update_pool_config +from chia.pools.pool_wallet_info import ( + PoolWalletInfo, + PoolSingletonState, + PoolState, + FARMING_TO_POOL, + SELF_POOLING, + LEAVING_POOL, + create_pool_state, +) +from chia.protocols.pool_protocol import POOL_PROTOCOL_VERSION + +from chia.types.announcement import Announcement +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.program import Program, SerializedProgram +from chia.types.coin_record import CoinRecord +from chia.types.coin_solution import CoinSolution +from chia.types.spend_bundle import SpendBundle + +from chia.pools.pool_puzzles import ( + create_waiting_room_inner_puzzle, + create_full_puzzle, + SINGLETON_LAUNCHER, + create_pooling_inner_puzzle, + solution_to_extra_data, + pool_state_to_inner_puzzle, + get_most_recent_singleton_coin_from_coin_solution, + launcher_id_to_p2_puzzle_hash, + create_travel_spend, + uncurry_pool_member_inner_puzzle, + create_absorb_spend, + is_pool_member_inner_puzzle, + is_pool_waitingroom_inner_puzzle, + uncurry_pool_waitingroom_inner_puzzle, + get_delayed_puz_info_from_launcher_spend, +) + +from chia.util.ints import uint8, uint32, uint64 +from chia.wallet.derive_keys import ( + master_sk_to_pooling_authentication_sk, + find_owner_sk, +) +from chia.wallet.sign_coin_solutions import sign_coin_solutions +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.wallet_types import WalletType +from chia.wallet.wallet import Wallet + +from chia.wallet.wallet_info import WalletInfo +from chia.wallet.util.transaction_type import TransactionType + + +class PoolWallet: + MINIMUM_INITIAL_BALANCE = 1 + MINIMUM_RELATIVE_LOCK_HEIGHT = 5 + MAXIMUM_RELATIVE_LOCK_HEIGHT = 1000 + + wallet_state_manager: Any + log: logging.Logger + wallet_info: WalletInfo + target_state: Optional[PoolState] + standard_wallet: Wallet + wallet_id: int + singleton_list: List[Coin] + """ + From the user's perspective, this is not a wallet at all, but a way to control + whether their pooling-enabled plots are being self-farmed, or farmed by a pool, + and by which pool. Self-pooling and joint pooling rewards are swept into the + users' regular wallet. + + If this wallet is in SELF_POOLING state, the coin ID associated with the current + pool wallet contains the rewards gained while self-farming, so care must be taken + to disallow joining a new pool while we still have money on the pooling singleton UTXO. + + Pools can be joined anonymously, without an account or prior signup. + + The ability to change the farm-to target prevents abuse from pools + by giving the user the ability to quickly change pools, or self-farm. + + The pool is also protected, by not allowing members to cheat by quickly leaving a pool, + and claiming a block that was pledged to the pool. + + The pooling protocol and smart coin prevents a user from quickly leaving a pool + by enforcing a wait time when leaving the pool. A minimum number of blocks must pass + after the user declares that they are leaving the pool, and before they can start to + self-claim rewards again. + + Control of switching states is granted to the owner public key. + + We reveal the inner_puzzle to the pool during setup of the pooling protocol. + The pool can prove to itself that the inner puzzle pays to the pooling address, + and it can follow state changes in the pooling puzzle by tracing destruction and + creation of coins associate with this pooling singleton (the singleton controlling + this pool group). + + The user trusts the pool to send mining rewards to the + TODO: We should mark which address is receiving funds for our current state. + + If the pool misbehaves, it is the user's responsibility to leave the pool + + It is the Pool's responsibility to claim the rewards sent to the pool_puzzlehash. + + The timeout for leaving the pool is expressed in number of blocks from the time + the user expresses their intent to leave. + + + + """ + + @classmethod + def type(cls) -> uint8: + return uint8(WalletType.POOLING_WALLET) + + def id(self): + return self.wallet_info.id + + @classmethod + def _verify_self_pooled(cls, state) -> Optional[str]: + err = "" + if state.pool_url != "": + err += " Unneeded pool_url for self-pooling" + + if state.relative_lock_height != 0: + err += " Incorrect relative_lock_height for self-pooling" + + return None if err == "" else err + + @classmethod + def _verify_pooling_state(cls, state) -> Optional[str]: + err = "" + if state.relative_lock_height < cls.MINIMUM_RELATIVE_LOCK_HEIGHT: + err += ( + f" Pool relative_lock_height ({state.relative_lock_height})" + f"is less than recommended minimum ({cls.MINIMUM_RELATIVE_LOCK_HEIGHT})" + ) + elif state.relative_lock_height > cls.MAXIMUM_RELATIVE_LOCK_HEIGHT: + err += ( + f" Pool relative_lock_height ({state.relative_lock_height})" + f"is greater than recommended maximum ({cls.MINIMUM_RELATIVE_LOCK_HEIGHT})" + ) + + if state.pool_url in [None, ""]: + err += " Empty pool url in pooling state" + return err + + @classmethod + def _verify_pool_state(cls, state: PoolState) -> Optional[str]: + if state.target_puzzle_hash is None: + return "Invalid puzzle_hash" + + if state.version > POOL_PROTOCOL_VERSION: + return ( + f"Detected pool protocol version {state.version}, which is " + f"newer than this wallet's version ({POOL_PROTOCOL_VERSION}). Please upgrade " + f"to use this pooling wallet" + ) + + if state.state == PoolSingletonState.SELF_POOLING: + return cls._verify_self_pooled(state) + elif state.state == PoolSingletonState.FARMING_TO_POOL or state.state == PoolSingletonState.LEAVING_POOL: + return cls._verify_pooling_state(state) + else: + return "Internal Error" + + @classmethod + def _verify_initial_target_state(cls, initial_target_state): + err = cls._verify_pool_state(initial_target_state) + if err: + raise ValueError(f"Invalid internal Pool State: {err}: {initial_target_state}") + + async def get_spend_history(self) -> List[Tuple[uint32, CoinSolution]]: + return self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id) + + async def get_current_state(self) -> PoolWalletInfo: + history: List[Tuple[uint32, CoinSolution]] = await self.get_spend_history() + all_spends: List[CoinSolution] = [cs for _, cs in history] + + # We must have at least the launcher spend + assert len(all_spends) >= 1 + + launcher_coin: Coin = all_spends[0].coin + delayed_seconds, delayed_puzhash = get_delayed_puz_info_from_launcher_spend(all_spends[0]) + tip_singleton_coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_solution(all_spends[-1]) + launcher_id: bytes32 = launcher_coin.name() + p2_singleton_puzzle_hash = launcher_id_to_p2_puzzle_hash(launcher_id, delayed_seconds, delayed_puzhash) + assert tip_singleton_coin is not None + + curr_spend_i = len(all_spends) - 1 + extra_data: Optional[PoolState] = None + last_singleton_spend_height = uint32(0) + while extra_data is None: + full_spend: CoinSolution = all_spends[curr_spend_i] + extra_data = solution_to_extra_data(full_spend) + last_singleton_spend_height = uint32(history[curr_spend_i][0]) + curr_spend_i -= 1 + + assert extra_data is not None + current_inner = pool_state_to_inner_puzzle( + extra_data, + launcher_coin.name(), + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + delayed_seconds, + delayed_puzhash, + ) + return PoolWalletInfo( + extra_data, + self.target_state, + launcher_coin, + launcher_id, + p2_singleton_puzzle_hash, + current_inner, + tip_singleton_coin.name(), + last_singleton_spend_height, + ) + + async def get_unconfirmed_transactions(self) -> List[TransactionRecord]: + return await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id) + + async def get_tip(self) -> Tuple[uint32, CoinSolution]: + return self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)[-1] + + async def update_pool_config(self, make_new_authentication_key: bool): + current_state: PoolWalletInfo = await self.get_current_state() + pool_config_list: List[PoolWalletConfig] = load_pool_config(self.wallet_state_manager.root_path) + pool_config_dict: Dict[bytes32, PoolWalletConfig] = {c.launcher_id: c for c in pool_config_list} + existing_config: Optional[PoolWalletConfig] = pool_config_dict.get(current_state.launcher_id, None) + + if make_new_authentication_key or existing_config is None: + new_auth_sk: PrivateKey = master_sk_to_pooling_authentication_sk( + self.wallet_state_manager.private_key, uint32(self.wallet_id), uint32(0) + ) + auth_pk: G1Element = new_auth_sk.get_g1() + payout_instructions: str = (await self.standard_wallet.get_new_puzzlehash(in_transaction=True)).hex() + else: + auth_pk = existing_config.authentication_public_key + payout_instructions = existing_config.payout_instructions + + new_config: PoolWalletConfig = PoolWalletConfig( + current_state.launcher_id, + current_state.current.pool_url if current_state.current.pool_url else "", + payout_instructions, + current_state.current.target_puzzle_hash, + current_state.p2_singleton_puzzle_hash, + current_state.current.owner_pubkey, + auth_pk, + ) + pool_config_dict[new_config.launcher_id] = new_config + await update_pool_config(self.wallet_state_manager.root_path, list(pool_config_dict.values())) + + @staticmethod + def get_next_interesting_coin_ids(spend: CoinSolution) -> List[bytes32]: + # CoinSolution of one of the coins that we cared about. This coin was spent in a block, but might be in a reorg + # If we return a value, it is a coin ID that we are also interested in (to support two transitions per block) + coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_solution(spend) + if coin is not None: + return [coin.name()] + return [] + + async def apply_state_transitions(self, block_spends: List[CoinSolution], block_height: uint32): + """ + Updates the Pool state (including DB) with new singleton spends. The block spends can contain many spends + that we are not interested in, and can contain many ephemeral spends. They must all be in the same block. + The DB must be committed after calling this method. All validation should be done here. + """ + coin_name_to_spend: Dict[bytes32, CoinSolution] = {cs.coin.name(): cs for cs in block_spends} + + tip: Tuple[uint32, CoinSolution] = await self.get_tip() + tip_height = tip[0] + tip_spend = tip[1] + assert block_height >= tip_height # We should not have a spend with a lesser block height + + while True: + tip_coin: Optional[Coin] = get_most_recent_singleton_coin_from_coin_solution(tip_spend) + assert tip_coin is not None + spent_coin_name: bytes32 = tip_coin.name() + if spent_coin_name not in coin_name_to_spend: + break + spend: CoinSolution = coin_name_to_spend[spent_coin_name] + await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, spend, block_height) + tip_spend = (await self.get_tip())[1] + self.log.info(f"New PoolWallet singleton tip_coin: {tip_spend}") + coin_name_to_spend.pop(spent_coin_name) + + # If we have reached the target state, resets it to None. Loops back to get current state + for _, added_spend in reversed(self.wallet_state_manager.pool_store.get_spends_for_wallet(self.wallet_id)): + latest_state: Optional[PoolState] = solution_to_extra_data(added_spend) + if latest_state is not None: + if self.target_state == latest_state: + self.target_state = None + break + await self.update_pool_config(False) + + async def rewind(self, block_height: int) -> bool: + """ + Rolls back all transactions after block_height, and if creation was after block_height, deletes the wallet. + Returns True if the wallet should be removed. + """ + try: + history: List[Tuple[uint32, CoinSolution]] = self.wallet_state_manager.pool_store.get_spends_for_wallet( + self.wallet_id + ).copy() + prev_state: PoolWalletInfo = await self.get_current_state() + await self.wallet_state_manager.pool_store.rollback(block_height, self.wallet_id) + + if len(history) > 0 and history[0][0] > block_height: + # If we have no entries in the DB, we have no singleton, so we should not have a wallet either + # The PoolWallet object becomes invalid after this. + await self.wallet_state_manager.interested_store.remove_interested_puzzle_hash( + prev_state.p2_singleton_puzzle_hash, in_transaction=True + ) + return True + else: + if await self.get_current_state() != prev_state: + await self.update_pool_config(False) + return False + except Exception as e: + self.log.error(f"Exception rewinding: {e}") + return False + + @staticmethod + async def create( + wallet_state_manager: Any, + wallet: Wallet, + launcher_coin_id: bytes32, + block_spends: List[CoinSolution], + block_height: uint32, + in_transaction: bool, + name: str = None, + ): + """ + This creates a new PoolWallet with only one spend: the launcher spend. The DB MUST be committed after calling + this method. + """ + self = PoolWallet() + self.wallet_state_manager = wallet_state_manager + + self.wallet_info = await wallet_state_manager.user_store.create_wallet( + "Pool wallet", WalletType.POOLING_WALLET.value, "", in_transaction=in_transaction + ) + self.wallet_id = self.wallet_info.id + self.standard_wallet = wallet + self.target_state = None + self.log = logging.getLogger(name if name else __name__) + + launcher_spend: Optional[CoinSolution] = None + for spend in block_spends: + if spend.coin.name() == launcher_coin_id: + launcher_spend = spend + assert launcher_spend is not None + await self.wallet_state_manager.pool_store.add_spend(self.wallet_id, launcher_spend, block_height) + await self.update_pool_config(True) + + p2_puzzle_hash: bytes32 = (await self.get_current_state()).p2_singleton_puzzle_hash + await self.wallet_state_manager.interested_store.add_interested_puzzle_hash( + p2_puzzle_hash, self.wallet_id, True + ) + + await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, create_puzzle_hashes=False) + self.wallet_state_manager.set_new_peak_callback(self.wallet_id, self.new_peak) + return self + + @staticmethod + async def create_from_db( + wallet_state_manager: Any, + wallet: Wallet, + wallet_info: WalletInfo, + name: str = None, + ): + """ + This creates a PoolWallet from DB. However, all data is already handled by WalletPoolStore, so we don't need + to do anything here. + """ + self = PoolWallet() + self.wallet_state_manager = wallet_state_manager + self.wallet_id = wallet_info.id + self.standard_wallet = wallet + self.wallet_info = wallet_info + self.target_state = None + self.log = logging.getLogger(name if name else __name__) + self.wallet_state_manager.set_new_peak_callback(self.wallet_id, self.new_peak) + return self + + @staticmethod + async def create_new_pool_wallet_transaction( + wallet_state_manager: Any, + main_wallet: Wallet, + initial_target_state: PoolState, + fee: uint64 = uint64(0), + p2_singleton_delay_time: Optional[uint64] = None, + p2_singleton_delayed_ph: Optional[bytes32] = None, + ) -> Tuple[TransactionRecord, bytes32, bytes32]: + """ + A "plot NFT", or pool wallet, represents the idea of a set of plots that all pay to + the same pooling puzzle. This puzzle is a `chia singleton` that is + parameterized with a public key controlled by the user's wallet + (a `smart coin`). It contains an inner puzzle that can switch between + paying block rewards to a pool, or to a user's own wallet. + + Call under the wallet state manger lock + """ + amount = 1 + standard_wallet = main_wallet + + if p2_singleton_delayed_ph is None: + p2_singleton_delayed_ph = await main_wallet.get_new_puzzlehash() + if p2_singleton_delay_time is None: + p2_singleton_delay_time = uint64(604800) + + unspent_records = await wallet_state_manager.coin_store.get_unspent_coins_for_wallet(standard_wallet.wallet_id) + balance = await standard_wallet.get_confirmed_balance(unspent_records) + if balance < PoolWallet.MINIMUM_INITIAL_BALANCE: + raise ValueError("Not enough balance in main wallet to create a managed plotting pool.") + if balance < fee: + raise ValueError("Not enough balance in main wallet to create a managed plotting pool with fee {fee}.") + + # Verify Parameters - raise if invalid + PoolWallet._verify_initial_target_state(initial_target_state) + + spend_bundle, singleton_puzzle_hash, launcher_coin_id = await PoolWallet.generate_launcher_spend( + standard_wallet, + uint64(1), + initial_target_state, + wallet_state_manager.constants.GENESIS_CHALLENGE, + p2_singleton_delay_time, + p2_singleton_delayed_ph, + ) + + if spend_bundle is None: + raise ValueError("failed to generate ID for wallet") + + standard_wallet_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=singleton_puzzle_hash, + amount=uint64(amount), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=spend_bundle.additions(), + removals=spend_bundle.removals(), + wallet_id=wallet_state_manager.main_wallet.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=spend_bundle.name(), + ) + await standard_wallet.push_transaction(standard_wallet_record) + p2_singleton_puzzle_hash: bytes32 = launcher_id_to_p2_puzzle_hash( + launcher_coin_id, p2_singleton_delay_time, p2_singleton_delayed_ph + ) + return standard_wallet_record, p2_singleton_puzzle_hash, launcher_coin_id + + async def sign(self, coin_solution: CoinSolution) -> SpendBundle: + async def pk_to_sk(pk: G1Element) -> PrivateKey: + owner_sk: Optional[PrivateKey] = await find_owner_sk([self.wallet_state_manager.private_key], pk) + assert owner_sk is not None + return owner_sk + + return await sign_coin_solutions( + [coin_solution], + pk_to_sk, + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA, + self.wallet_state_manager.constants.MAX_BLOCK_COST_CLVM, + ) + + async def generate_travel_transaction(self) -> TransactionRecord: + # target_state is contained within pool_wallet_state + pool_wallet_info: PoolWalletInfo = await self.get_current_state() + + spend_history = await self.get_spend_history() + last_coin_solution: CoinSolution = spend_history[-1][1] + delayed_seconds, delayed_puzhash = get_delayed_puz_info_from_launcher_spend(spend_history[0][1]) + assert pool_wallet_info.target is not None + next_state = pool_wallet_info.target + if pool_wallet_info.current.state in [FARMING_TO_POOL]: + next_state = create_pool_state( + LEAVING_POOL, + pool_wallet_info.current.target_puzzle_hash, + pool_wallet_info.current.owner_pubkey, + pool_wallet_info.current.pool_url, + pool_wallet_info.current.relative_lock_height, + ) + + new_inner_puzzle = pool_state_to_inner_puzzle( + next_state, + pool_wallet_info.launcher_coin.name(), + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + delayed_seconds, + delayed_puzhash, + ) + new_full_puzzle: SerializedProgram = SerializedProgram.from_program( + create_full_puzzle(new_inner_puzzle, pool_wallet_info.launcher_coin.name()) + ) + + outgoing_coin_solution, inner_puzzle = create_travel_spend( + last_coin_solution, + pool_wallet_info.launcher_coin, + pool_wallet_info.current, + next_state, + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + delayed_seconds, + delayed_puzhash, + ) + + tip = (await self.get_tip())[1] + tip_coin = tip.coin + singleton = tip.additions()[0] + singleton_id = singleton.name() + assert outgoing_coin_solution.coin.parent_coin_info == tip_coin.name() + assert outgoing_coin_solution.coin.name() == singleton_id + assert new_inner_puzzle != inner_puzzle + if is_pool_member_inner_puzzle(inner_puzzle): + ( + inner_f, + target_puzzle_hash, + p2_singleton_hash, + pubkey_as_program, + pool_reward_prefix, + escape_puzzle_hash, + ) = uncurry_pool_member_inner_puzzle(inner_puzzle) + pk_bytes: bytes = bytes(pubkey_as_program.as_atom()) + assert len(pk_bytes) == 48 + owner_pubkey = G1Element.from_bytes(pk_bytes) + assert owner_pubkey == pool_wallet_info.current.owner_pubkey + elif is_pool_waitingroom_inner_puzzle(inner_puzzle): + ( + target_puzzle_hash, # payout_puzzle_hash + relative_lock_height, + owner_pubkey, + p2_singleton_hash, + ) = uncurry_pool_waitingroom_inner_puzzle(inner_puzzle) + pk_bytes = bytes(owner_pubkey.as_atom()) + assert len(pk_bytes) == 48 + assert owner_pubkey == pool_wallet_info.current.owner_pubkey + else: + raise RuntimeError("Invalid state") + + signed_spend_bundle = await self.sign(outgoing_coin_solution) + + assert signed_spend_bundle.removals()[0].puzzle_hash == singleton.puzzle_hash + assert signed_spend_bundle.removals()[0].name() == singleton.name() + assert signed_spend_bundle is not None + + tx_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=new_full_puzzle.get_tree_hash(), + amount=uint64(1), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=signed_spend_bundle, + additions=signed_spend_bundle.additions(), + removals=signed_spend_bundle.removals(), + wallet_id=self.id(), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=signed_spend_bundle.name(), + ) + return tx_record + + @staticmethod + async def generate_launcher_spend( + standard_wallet: Wallet, + amount: uint64, + initial_target_state: PoolState, + genesis_challenge: bytes32, + delay_time: uint64, + delay_ph: bytes32, + ) -> Tuple[SpendBundle, bytes32, bytes32]: + """ + Creates the initial singleton, which includes spending an origin coin, the launcher, and creating a singleton + with the "pooling" inner state, which can be either self pooling or using a pool + """ + + coins: Set[Coin] = await standard_wallet.select_coins(amount) + if coins is None: + raise ValueError("Not enough coins to create pool wallet") + + assert len(coins) == 1 + + launcher_parent: Coin = coins.copy().pop() + genesis_launcher_puz: Program = SINGLETON_LAUNCHER + launcher_coin: Coin = Coin(launcher_parent.name(), genesis_launcher_puz.get_tree_hash(), amount) + + escaping_inner_puzzle: bytes32 = create_waiting_room_inner_puzzle( + initial_target_state.target_puzzle_hash, + initial_target_state.relative_lock_height, + initial_target_state.owner_pubkey, + launcher_coin.name(), + genesis_challenge, + delay_time, + delay_ph, + ) + escaping_inner_puzzle_hash = escaping_inner_puzzle.get_tree_hash() + + self_pooling_inner_puzzle: Program = create_pooling_inner_puzzle( + initial_target_state.target_puzzle_hash, + escaping_inner_puzzle_hash, + initial_target_state.owner_pubkey, + launcher_coin.name(), + genesis_challenge, + delay_time, + delay_ph, + ) + + if initial_target_state.state == SELF_POOLING: + puzzle = escaping_inner_puzzle + elif initial_target_state.state == FARMING_TO_POOL: + puzzle = self_pooling_inner_puzzle + else: + raise ValueError("Invalid initial state") + full_pooling_puzzle: Program = create_full_puzzle(puzzle, launcher_id=launcher_coin.name()) + + puzzle_hash: bytes32 = full_pooling_puzzle.get_tree_hash() + extra_data_bytes = Program.to([("p", bytes(initial_target_state)), ("t", delay_time), ("h", delay_ph)]) + announcement_set: Set[Announcement] = set() + announcement_message = Program.to([puzzle_hash, amount, extra_data_bytes]).get_tree_hash() + announcement_set.add(Announcement(launcher_coin.name(), announcement_message).name()) + + create_launcher_tx_record: Optional[TransactionRecord] = await standard_wallet.generate_signed_transaction( + amount, + genesis_launcher_puz.get_tree_hash(), + uint64(0), + None, + coins, + None, + False, + announcement_set, + ) + assert create_launcher_tx_record is not None and create_launcher_tx_record.spend_bundle is not None + + genesis_launcher_solution: Program = Program.to([puzzle_hash, amount, extra_data_bytes]) + + launcher_cs: CoinSolution = CoinSolution( + launcher_coin, + SerializedProgram.from_program(genesis_launcher_puz), + SerializedProgram.from_program(genesis_launcher_solution), + ) + launcher_sb: SpendBundle = SpendBundle([launcher_cs], G2Element()) + + # Current inner will be updated when state is verified on the blockchain + full_spend: SpendBundle = SpendBundle.aggregate([create_launcher_tx_record.spend_bundle, launcher_sb]) + return full_spend, puzzle_hash, launcher_coin.name() + + async def join_pool(self, target_state: PoolState): + + if target_state.state != FARMING_TO_POOL: + raise ValueError(f"join_pool must be called with target_state={FARMING_TO_POOL} (FARMING_TO_POOL)") + if self.target_state is not None: + raise ValueError(f"Cannot join a pool while waiting for target state: {self.target_state}") + if await self.have_unconfirmed_transaction(): + raise ValueError( + "Cannot claim due to unconfirmed transaction. If this is stuck, delete the unconfirmed transaction." + ) + + current_state: PoolWalletInfo = await self.get_current_state() + + if current_state.current == target_state: + self.target_state = None + self.log.info("Asked to change to current state. Target = {target_state}") + return + + if self.target_state is not None: + raise ValueError( + f"Cannot change to state {target_state} when already having target state: {self.target_state}" + ) + PoolWallet._verify_initial_target_state(target_state) + if current_state.current.state == LEAVING_POOL: + history: List[Tuple[uint32, CoinSolution]] = await self.get_spend_history() + last_height: uint32 = history[-1][0] + if self.wallet_state_manager.get_peak().height <= last_height + current_state.current.relative_lock_height: + raise ValueError( + f"Cannot join a pool until height {last_height + current_state.current.relative_lock_height}" + ) + + self.target_state = target_state + tx_record: TransactionRecord = await self.generate_travel_transaction() + await self.wallet_state_manager.add_pending_transaction(tx_record) + + return tx_record + + async def self_pool(self): + if await self.have_unconfirmed_transaction(): + raise ValueError( + "Cannot claim due to unconfirmed transaction. If this is stuck, delete the unconfirmed transaction." + ) + pool_wallet_info: PoolWalletInfo = await self.get_current_state() + if pool_wallet_info.current.state == SELF_POOLING: + raise ValueError("Attempted to self pool when already self pooling") + + if self.target_state is not None: + raise ValueError(f"Cannot self pool when already having target state: {self.target_state}") + + # Note the implications of getting owner_puzzlehash from our local wallet right now + # vs. having pre-arranged the target self-pooling address + owner_puzzlehash = await self.standard_wallet.get_new_puzzlehash() + owner_pubkey = pool_wallet_info.current.owner_pubkey + current_state: PoolWalletInfo = await self.get_current_state() + + if current_state.current.state == LEAVING_POOL: + history: List[Tuple[uint32, CoinSolution]] = await self.get_spend_history() + last_height: uint32 = history[-1][0] + if self.wallet_state_manager.get_peak().height <= last_height + current_state.current.relative_lock_height: + raise ValueError( + f"Cannot self pool until height {last_height + current_state.current.relative_lock_height}" + ) + self.target_state = create_pool_state( + SELF_POOLING, owner_puzzlehash, owner_pubkey, pool_url=None, relative_lock_height=uint32(0) + ) + tx_record = await self.generate_travel_transaction() + await self.wallet_state_manager.add_pending_transaction(tx_record) + return tx_record + + async def claim_pool_rewards(self, fee: uint64) -> TransactionRecord: + # Search for p2_puzzle_hash coins, and spend them with the singleton + if await self.have_unconfirmed_transaction(): + raise ValueError( + "Cannot claim due to unconfirmed transaction. If this is stuck, delete the unconfirmed transaction." + ) + + unspent_coin_records: List[CoinRecord] = list( + await self.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(self.wallet_id) + ) + + if len(unspent_coin_records) == 0: + raise ValueError("Nothing to claim") + farming_rewards: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_farming_rewards() + coin_to_height_farmed: Dict[Coin, uint32] = {} + for tx_record in farming_rewards: + height_farmed: Optional[uint32] = tx_record.height_farmed( + self.wallet_state_manager.constants.GENESIS_CHALLENGE + ) + assert height_farmed is not None + coin_to_height_farmed[tx_record.additions[0]] = height_farmed + history: List[Tuple[uint32, CoinSolution]] = await self.get_spend_history() + assert len(history) > 0 + delayed_seconds, delayed_puzhash = get_delayed_puz_info_from_launcher_spend(history[0][1]) + current_state: PoolWalletInfo = await self.get_current_state() + last_solution: CoinSolution = history[-1][1] + + all_spends: List[CoinSolution] = [] + total_amount = 0 + for coin_record in unspent_coin_records: + if len(all_spends) >= 100: + # Limit the total number of spends, so it fits into the block + break + absorb_spend: List[CoinSolution] = create_absorb_spend( + last_solution, + current_state.current, + current_state.launcher_coin, + coin_to_height_farmed[coin_record.coin], + self.wallet_state_manager.constants.GENESIS_CHALLENGE, + delayed_seconds, + delayed_puzhash, + ) + last_solution = absorb_spend[0] + all_spends += absorb_spend + total_amount += coin_record.coin.amount + self.log.info( + f"Farmer coin: {coin_record.coin} {coin_record.coin.name()} {coin_to_height_farmed[coin_record.coin]}" + ) + + # No signatures are required to absorb + spend_bundle: SpendBundle = SpendBundle(all_spends, G2Element()) + + absorb_transaction: TransactionRecord = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=current_state.current.target_puzzle_hash, + amount=uint64(total_amount), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=spend_bundle.additions(), + removals=spend_bundle.removals(), + wallet_id=uint32(self.wallet_id), + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=spend_bundle.name(), + ) + await self.wallet_state_manager.add_pending_transaction(absorb_transaction) + return absorb_transaction + + async def new_peak(self, peak: BlockRecord) -> None: + # This gets called from the WalletStateManager whenever there is a new peak + + pool_wallet_info: PoolWalletInfo = await self.get_current_state() + tip_height, tip_spend = await self.get_tip() + + if self.target_state is None: + return + if self.target_state == pool_wallet_info.current.state: + self.target_state = None + raise ValueError("Internal error") + + if ( + self.target_state.state in [FARMING_TO_POOL, SELF_POOLING] + and pool_wallet_info.current.state == LEAVING_POOL + ): + leave_height = tip_height + pool_wallet_info.current.relative_lock_height + + curr: BlockRecord = peak + while not curr.is_transaction_block: + curr = self.wallet_state_manager.blockchain.block_record(curr.prev_hash) + + self.log.info(f"Last transaction block height: {curr.height} OK to leave at height {leave_height}") + + # Add some buffer (+2) to reduce chances of a reorg + if curr.height > leave_height + 2: + unconfirmed: List[ + TransactionRecord + ] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(self.wallet_id) + next_tip: Optional[Coin] = get_most_recent_singleton_coin_from_coin_solution(tip_spend) + assert next_tip is not None + + if any([rem.name() == next_tip.name() for tx_rec in unconfirmed for rem in tx_rec.removals]): + self.log.info("Already submitted second transaction, will not resubmit.") + return + + self.log.info(f"Attempting to leave from\n{pool_wallet_info.current}\nto\n{self.target_state}") + assert self.target_state.version == POOL_PROTOCOL_VERSION + assert pool_wallet_info.current.state == LEAVING_POOL + assert self.target_state.target_puzzle_hash is not None + + if self.target_state.state == SELF_POOLING: + assert self.target_state.relative_lock_height == 0 + assert self.target_state.pool_url is None + elif self.target_state.state == FARMING_TO_POOL: + assert self.target_state.relative_lock_height >= self.MINIMUM_RELATIVE_LOCK_HEIGHT + assert self.target_state.pool_url is not None + + tx_record = await self.generate_travel_transaction() + await self.wallet_state_manager.add_pending_transaction(tx_record) + + async def have_unconfirmed_transaction(self) -> bool: + unconfirmed: List[TransactionRecord] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet( + self.wallet_id + ) + return len(unconfirmed) > 0 + + async def get_confirmed_balance(self, record_list=None) -> uint64: + if (await self.get_current_state()).current.state == SELF_POOLING: + return await self.wallet_state_manager.get_confirmed_balance_for_wallet(self.wallet_id, record_list) + else: + return uint64(0) + + async def get_unconfirmed_balance(self, record_list=None) -> uint64: + return await self.get_confirmed_balance(record_list) + + async def get_spendable_balance(self, record_list=None) -> uint64: + return await self.get_confirmed_balance(record_list) + + async def get_pending_change_balance(self) -> uint64: + return uint64(0) + + async def get_max_send_amount(self, record_list=None) -> uint64: + return uint64(0) diff --git a/chia/pools/pool_wallet_info.py b/chia/pools/pool_wallet_info.py new file mode 100644 index 000000000000..acf906216baf --- /dev/null +++ b/chia/pools/pool_wallet_info.py @@ -0,0 +1,115 @@ +from dataclasses import dataclass +from enum import IntEnum +from typing import Optional, Dict + +from blspy import G1Element + +from chia.protocols.pool_protocol import POOL_PROTOCOL_VERSION +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.byte_types import hexstr_to_bytes +from chia.util.ints import uint32, uint8 +from chia.util.streamable import streamable, Streamable + + +class PoolSingletonState(IntEnum): + """ + From the user's point of view, a pool group can be in these states: + `SELF_POOLING`: The singleton exists on the blockchain, and we are farming + block rewards to a wallet address controlled by the user + + `LEAVING_POOL`: The singleton exists, and we have entered the "escaping" state, which + means we are waiting for a number of blocks = `relative_lock_height` to pass, so we can leave. + + `FARMING_TO_POOL`: The singleton exists, and it is assigned to a pool. + + `CLAIMING_SELF_POOLED_REWARDS`: We have submitted a transaction to sweep our + self-pooled funds. + """ + + SELF_POOLING = 1 + LEAVING_POOL = 2 + FARMING_TO_POOL = 3 + + +SELF_POOLING = PoolSingletonState.SELF_POOLING +LEAVING_POOL = PoolSingletonState.LEAVING_POOL +FARMING_TO_POOL = PoolSingletonState.FARMING_TO_POOL + + +@dataclass(frozen=True) +@streamable +class PoolState(Streamable): + """ + `PoolState` is a type that is serialized to the blockchain to track the state of the user's pool singleton + `target_puzzle_hash` is either the pool address, or the self-pooling address that pool rewards will be paid to. + `target_puzzle_hash` is NOT the p2_singleton puzzle that block rewards are sent to. + The `p2_singleton` address is the initial address, and the `target_puzzle_hash` is the final destination. + `relative_lock_height` is zero when in SELF_POOLING state + """ + + version: uint8 + state: uint8 # PoolSingletonState + # `target_puzzle_hash`: A puzzle_hash we pay to + # When self-farming, this is a main wallet address + # When farming-to-pool, the pool sends this to the farmer during pool protocol setup + target_puzzle_hash: bytes32 # TODO: rename target_puzzle_hash -> pay_to_address + # owner_pubkey is set by the wallet, once + owner_pubkey: G1Element + pool_url: Optional[str] + relative_lock_height: uint32 + + +def initial_pool_state_from_dict(state_dict: Dict, owner_pubkey: G1Element, owner_puzzle_hash: bytes32) -> PoolState: + state_str = state_dict["state"] + singleton_state: PoolSingletonState = PoolSingletonState[state_str] + + if singleton_state == SELF_POOLING: + target_puzzle_hash = owner_puzzle_hash + pool_url: str = "" + relative_lock_height = uint32(0) + elif singleton_state == FARMING_TO_POOL: + target_puzzle_hash = bytes32(hexstr_to_bytes(state_dict["target_puzzle_hash"])) + pool_url = state_dict["pool_url"] + relative_lock_height = uint32(state_dict["relative_lock_height"]) + else: + raise ValueError("Initial state must be SELF_POOLING or FARMING_TO_POOL") + + # TODO: change create_pool_state to return error messages, as well + assert relative_lock_height is not None + return create_pool_state(singleton_state, target_puzzle_hash, owner_pubkey, pool_url, relative_lock_height) + + +def create_pool_state( + state: PoolSingletonState, + target_puzzle_hash: bytes32, + owner_pubkey: G1Element, + pool_url: Optional[str], + relative_lock_height: uint32, +) -> PoolState: + if state not in set(s.value for s in PoolSingletonState): + raise AssertionError("state {state} is not a valid PoolSingletonState,") + ps = PoolState( + POOL_PROTOCOL_VERSION, uint8(state), target_puzzle_hash, owner_pubkey, pool_url, relative_lock_height + ) + # TODO Move verify here + return ps + + +@dataclass(frozen=True) +@streamable +class PoolWalletInfo(Streamable): + """ + Internal Pool Wallet state, not destined for the blockchain. This can be completely derived with + the Singleton's CoinSolutions list, or with the information from the WalletPoolStore. + """ + + current: PoolState + target: Optional[PoolState] + launcher_coin: Coin + launcher_id: bytes32 + p2_singleton_puzzle_hash: bytes32 + current_inner: Program # Inner puzzle in current singleton, not revealed yet + tip_singleton_coin_id: bytes32 + singleton_block_height: uint32 # Block height that current PoolState is from diff --git a/chia/protocols/harvester_protocol.py b/chia/protocols/harvester_protocol.py index d7db668f1ea0..b48165773fc9 100644 --- a/chia/protocols/harvester_protocol.py +++ b/chia/protocols/harvester_protocol.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import List, Tuple +from typing import List, Tuple, Optional from blspy import G1Element, G2Element @@ -14,6 +14,14 @@ Note: When changing this file, also change protocol_message_types.py, and the pr """ +@dataclass(frozen=True) +@streamable +class PoolDifficulty(Streamable): + difficulty: uint64 + sub_slot_iters: uint64 + pool_contract_puzzle_hash: bytes32 + + @dataclass(frozen=True) @streamable class HarvesterHandshake(Streamable): @@ -29,6 +37,7 @@ class NewSignagePointHarvester(Streamable): sub_slot_iters: uint64 signage_point_index: uint8 sp_hash: bytes32 + pool_difficulties: List[PoolDifficulty] @dataclass(frozen=True) @@ -59,3 +68,30 @@ class RespondSignatures(Streamable): local_pk: G1Element farmer_pk: G1Element message_signatures: List[Tuple[bytes32, G2Element]] + + +@dataclass(frozen=True) +@streamable +class Plot(Streamable): + filename: str + size: uint8 + plot_id: bytes32 + pool_public_key: Optional[G1Element] + pool_contract_puzzle_hash: Optional[bytes32] + plot_public_key: G1Element + file_size: uint64 + time_modified: uint64 + + +@dataclass(frozen=True) +@streamable +class RequestPlots(Streamable): + pass + + +@dataclass(frozen=True) +@streamable +class RespondPlots(Streamable): + plots: List[Plot] + failed_to_open_filenames: List[str] + no_key_filenames: List[str] diff --git a/chia/protocols/pool_protocol.py b/chia/protocols/pool_protocol.py index caa6a8922cdc..e6a1f7f8ff15 100644 --- a/chia/protocols/pool_protocol.py +++ b/chia/protocols/pool_protocol.py @@ -1,50 +1,175 @@ from dataclasses import dataclass -from typing import List, Optional +from enum import Enum +import time +from typing import Optional + +from blspy import G1Element, G2Element from chia.types.blockchain_format.proof_of_space import ProofOfSpace -from chia.util.ints import uint32, uint64 +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.ints import uint8, uint16, uint32, uint64 from chia.util.streamable import Streamable, streamable -""" -Protocol between farmer and pool. -Note: When changing this file, also change protocol_message_types.py, and the protocol version in shared_protocol.py -""" +POOL_PROTOCOL_VERSION = uint8(1) + + +class PoolErrorCode(Enum): + REVERTED_SIGNAGE_POINT = 1 + TOO_LATE = 2 + NOT_FOUND = 3 + INVALID_PROOF = 4 + PROOF_NOT_GOOD_ENOUGH = 5 + INVALID_DIFFICULTY = 6 + INVALID_SIGNATURE = 7 + SERVER_EXCEPTION = 8 + INVALID_P2_SINGLETON_PUZZLE_HASH = 9 + FARMER_NOT_KNOWN = 10 + FARMER_ALREADY_KNOWN = 11 + INVALID_AUTHENTICATION_TOKEN = 12 + INVALID_PAYOUT_INSTRUCTIONS = 13 + INVALID_SINGLETON = 14 + DELAY_TIME_TOO_SHORT = 15 + REQUEST_FAILED = 16 + + +# Used to verify GET /farmer and GET /login +@dataclass(frozen=True) +@streamable +class AuthenticationPayload(Streamable): + method_name: str + launcher_id: bytes32 + target_puzzle_hash: bytes32 + authentication_token: uint64 + + +# GET /pool_info +@dataclass(frozen=True) +@streamable +class GetPoolInfoResponse(Streamable): + name: str + logo_url: str + minimum_difficulty: uint64 + relative_lock_height: uint32 + protocol_version: uint8 + fee: str + description: str + target_puzzle_hash: bytes32 + authentication_token_timeout: uint8 + + +# POST /partial @dataclass(frozen=True) @streamable -class SignedCoinbase(Streamable): - pass - # coinbase_signature: PrependSignature - - -@dataclass(frozen=True) -@streamable -class RequestData(Streamable): - min_height: Optional[uint32] - farmer_id: Optional[str] - - -@dataclass(frozen=True) -@streamable -class RespondData(Streamable): - posting_url: str - # pool_public_key: PublicKey - partials_threshold: uint64 - coinbase_info: List[SignedCoinbase] - - -@dataclass(frozen=True) -@streamable -class Partial(Streamable): - # challenge: Challenge +class PostPartialPayload(Streamable): + launcher_id: bytes32 + authentication_token: uint64 proof_of_space: ProofOfSpace - farmer_target: str - # Signature of the challenge + farmer target hash - # signature: PrependSignature + sp_hash: bytes32 + end_of_sub_slot: bool + harvester_id: bytes32 @dataclass(frozen=True) @streamable -class PartialAck(Streamable): - pass +class PostPartialRequest(Streamable): + payload: PostPartialPayload + aggregate_signature: G2Element + + +# Response in success case +@dataclass(frozen=True) +@streamable +class PostPartialResponse(Streamable): + new_difficulty: uint64 + + +# GET /farmer + + +# Response in success case +@dataclass(frozen=True) +@streamable +class GetFarmerResponse(Streamable): + authentication_public_key: G1Element + payout_instructions: str + current_difficulty: uint64 + current_points: uint64 + + +# POST /farmer + + +@dataclass(frozen=True) +@streamable +class PostFarmerPayload(Streamable): + launcher_id: bytes32 + authentication_token: uint64 + authentication_public_key: G1Element + payout_instructions: str + suggested_difficulty: Optional[uint64] + + +@dataclass(frozen=True) +@streamable +class PostFarmerRequest(Streamable): + payload: PostFarmerPayload + signature: G2Element + + +# Response in success case +@dataclass(frozen=True) +@streamable +class PostFarmerResponse(Streamable): + welcome_message: str + + +# PUT /farmer + + +@dataclass(frozen=True) +@streamable +class PutFarmerPayload(Streamable): + launcher_id: bytes32 + authentication_token: uint64 + authentication_public_key: Optional[G1Element] + payout_instructions: Optional[str] + suggested_difficulty: Optional[uint64] + + +@dataclass(frozen=True) +@streamable +class PutFarmerRequest(Streamable): + payload: PutFarmerPayload + signature: G2Element + + +# Response in success case +@dataclass(frozen=True) +@streamable +class PutFarmerResponse(Streamable): + authentication_public_key: Optional[bool] + payout_instructions: Optional[bool] + suggested_difficulty: Optional[bool] + + +# Misc + + +# Response in error case for all endpoints of the pool protocol +@dataclass(frozen=True) +@streamable +class ErrorResponse(Streamable): + error_code: uint16 + error_message: Optional[str] + + +# Get the current authentication toke according "Farmer authentication" in SPECIFICATION.md +def get_current_authentication_token(timeout: uint8) -> uint64: + return uint64(int(int(time.time() / 60) / timeout)) + + +# Validate a given authentication token against our local time +def validate_authentication_token(token: uint64, timeout: uint8): + return abs(token - get_current_authentication_token(timeout)) <= timeout diff --git a/chia/protocols/protocol_message_types.py b/chia/protocols/protocol_message_types.py index e12a92115fa4..93781a28fb64 100644 --- a/chia/protocols/protocol_message_types.py +++ b/chia/protocols/protocol_message_types.py @@ -7,7 +7,7 @@ class ProtocolMessageTypes(Enum): # Harvester protocol (harvester <-> farmer) harvester_handshake = 3 - new_signage_point_harvester = 4 + # new_signage_point_harvester = 4 Changed to 66 in new protocol new_proof_of_space = 5 request_signatures = 6 respond_signatures = 7 @@ -81,3 +81,8 @@ class ProtocolMessageTypes(Enum): # Simulator protocol farm_new_block = 65 + + # New harvester protocol + new_signage_point_harvester = 66 + request_plots = 67 + respond_plots = 68 diff --git a/chia/rpc/farmer_rpc_api.py b/chia/rpc/farmer_rpc_api.py index 697854a22f47..5f48de8f8344 100644 --- a/chia/rpc/farmer_rpc_api.py +++ b/chia/rpc/farmer_rpc_api.py @@ -1,6 +1,7 @@ -from typing import Callable, Dict, List +from typing import Callable, Dict, List, Optional from chia.farmer.farmer import Farmer +from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.byte_types import hexstr_to_bytes from chia.util.ws_message import WsRpcMessage, create_payload_dict @@ -16,6 +17,10 @@ class FarmerRpcApi: "/get_signage_points": self.get_signage_points, "/get_reward_targets": self.get_reward_targets, "/set_reward_targets": self.set_reward_targets, + "/get_pool_state": self.get_pool_state, + "/set_payout_instructions": self.set_payout_instructions, + "/get_plots": self.get_plots, + "/get_pool_login_link": self.get_pool_login_link, } async def _state_changed(self, change: str, change_data: Dict) -> List[WsRpcMessage]: @@ -93,3 +98,26 @@ class FarmerRpcApi: self.service.set_reward_targets(farmer_target, pool_target) return {} + + async def get_pool_state(self, _: Dict) -> Dict: + pools_list = [] + for p2_singleton_puzzle_hash, pool_dict in self.service.pool_state.items(): + pool_state = pool_dict.copy() + pool_state["p2_singleton_puzzle_hash"] = p2_singleton_puzzle_hash.hex() + pools_list.append(pool_state) + return {"pool_state": pools_list} + + async def set_payout_instructions(self, request: Dict) -> Dict: + launcher_id: bytes32 = hexstr_to_bytes(request["launcher_id"]) + await self.service.set_payout_instructions(launcher_id, request["payout_instructions"]) + return {} + + async def get_plots(self, _: Dict): + return await self.service.get_plots() + + async def get_pool_login_link(self, request: Dict) -> Dict: + launcher_id: bytes32 = bytes32(hexstr_to_bytes(request["launcher_id"])) + login_link: Optional[str] = await self.service.generate_login_link(launcher_id) + if login_link is None: + raise ValueError(f"Failed to generate login link for {launcher_id.hex()}") + return {"login_link": login_link} diff --git a/chia/rpc/farmer_rpc_client.py b/chia/rpc/farmer_rpc_client.py index 82f0cfb470eb..7b1bc8f67ca8 100644 --- a/chia/rpc/farmer_rpc_client.py +++ b/chia/rpc/farmer_rpc_client.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Any from chia.rpc.rpc_client import RpcClient from chia.types.blockchain_format.sized_bytes import bytes32 @@ -41,3 +41,19 @@ class FarmerRpcClient(RpcClient): if pool_target is not None: request["pool_target"] = pool_target return await self.fetch("set_reward_targets", request) + + async def get_pool_state(self) -> Dict: + return await self.fetch("get_pool_state", {}) + + async def set_payout_instructions(self, launcher_id: bytes32, payout_instructions: str) -> Dict: + request = {"launcher_id": launcher_id.hex(), "payout_instructions": payout_instructions} + return await self.fetch("set_payout_instructions", request) + + async def get_plots(self) -> Dict[str, Any]: + return await self.fetch("get_plots", {}) + + async def get_pool_login_link(self, launcher_id: bytes32) -> Optional[str]: + try: + return (await self.fetch("get_pool_login_link", {"launcher_id": launcher_id.hex()}))["login_link"] + except ValueError: + return None diff --git a/chia/rpc/full_node_rpc_api.py b/chia/rpc/full_node_rpc_api.py index cd51ffd9d55f..d95ef28c745c 100644 --- a/chia/rpc/full_node_rpc_api.py +++ b/chia/rpc/full_node_rpc_api.py @@ -3,9 +3,13 @@ from typing import Any, Callable, Dict, List, Optional from chia.consensus.block_record import BlockRecord from chia.consensus.pos_quality import UI_ACTUAL_SPACE_CONSTANT_FACTOR from chia.full_node.full_node import FullNode +from chia.full_node.mempool_check_conditions import get_puzzle_and_solution_for_coin +from chia.types.blockchain_format.program import Program, SerializedProgram from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord +from chia.types.coin_solution import CoinSolution from chia.types.full_block import FullBlock +from chia.types.generator_types import BlockGenerator from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.spend_bundle import SpendBundle from chia.types.unfinished_header_block import UnfinishedHeaderBlock @@ -34,11 +38,13 @@ class FullNodeRpcApi: "/get_additions_and_removals": self.get_additions_and_removals, "/get_initial_freeze_period": self.get_initial_freeze_period, "/get_network_info": self.get_network_info, + "/get_recent_signage_point_or_eos": self.get_recent_signage_point_or_eos, # Coins "/get_coin_records_by_puzzle_hash": self.get_coin_records_by_puzzle_hash, "/get_coin_records_by_puzzle_hashes": self.get_coin_records_by_puzzle_hashes, "/get_coin_record_by_name": self.get_coin_record_by_name, "/push_tx": self.push_tx, + "/get_puzzle_and_solution": self.get_puzzle_and_solution, # Mempool "/get_all_mempool_tx_ids": self.get_all_mempool_tx_ids, "/get_all_mempool_items": self.get_all_mempool_items, @@ -161,6 +167,95 @@ class FullNodeRpcApi: address_prefix = self.service.config["network_overrides"]["config"][network_name]["address_prefix"] return {"network_name": network_name, "network_prefix": address_prefix} + async def get_recent_signage_point_or_eos(self, request: Dict): + if "sp_hash" not in request: + challenge_hash: bytes32 = hexstr_to_bytes(request["challenge_hash"]) + # This is the case of getting an end of slot + eos_tuple = self.service.full_node_store.recent_eos.get(challenge_hash) + if not eos_tuple: + raise ValueError(f"Did not find eos {challenge_hash.hex()} in cache") + eos, time_received = eos_tuple + + # If it's still in the full node store, it's not reverted + if self.service.full_node_store.get_sub_slot(eos.challenge_chain.get_hash()): + return {"eos": eos, "time_received": time_received, "reverted": False} + + # Otherwise we can backtrack from peak to find it in the blockchain + curr: Optional[BlockRecord] = self.service.blockchain.get_peak() + if curr is None: + raise ValueError("No blocks in the chain") + + number_of_slots_searched = 0 + while number_of_slots_searched < 10: + if curr.first_in_sub_slot: + assert curr.finished_challenge_slot_hashes is not None + if curr.finished_challenge_slot_hashes[-1] == eos.challenge_chain.get_hash(): + # Found this slot in the blockchain + return {"eos": eos, "time_received": time_received, "reverted": False} + number_of_slots_searched += len(curr.finished_challenge_slot_hashes) + curr = self.service.blockchain.try_block_record(curr.prev_hash) + if curr is None: + # Got to the beginning of the blockchain without finding the slot + return {"eos": eos, "time_received": time_received, "reverted": True} + + # Backtracked through 10 slots but still did not find it + return {"eos": eos, "time_received": time_received, "reverted": True} + + # Now we handle the case of getting a signage point + sp_hash: bytes32 = hexstr_to_bytes(request["sp_hash"]) + sp_tuple = self.service.full_node_store.recent_signage_points.get(sp_hash) + if sp_tuple is None: + raise ValueError(f"Did not find sp {sp_hash.hex()} in cache") + + sp, time_received = sp_tuple + + # If it's still in the full node store, it's not reverted + if self.service.full_node_store.get_signage_point(sp_hash): + return {"signage_point": sp, "time_received": time_received, "reverted": False} + + # Otherwise we can backtrack from peak to find it in the blockchain + rc_challenge: bytes32 = sp.rc_vdf.challenge + next_b: Optional[BlockRecord] = None + curr_b_optional: Optional[BlockRecord] = self.service.blockchain.get_peak() + assert curr_b_optional is not None + curr_b: BlockRecord = curr_b_optional + + for _ in range(200): + sp_total_iters = sp.cc_vdf.number_of_iterations + curr_b.ip_sub_slot_total_iters(self.service.constants) + if curr_b.reward_infusion_new_challenge == rc_challenge: + if next_b is None: + return {"signage_point": sp, "time_received": time_received, "reverted": False} + next_b_total_iters = next_b.ip_sub_slot_total_iters(self.service.constants) + next_b.ip_iters( + self.service.constants + ) + + return { + "signage_point": sp, + "time_received": time_received, + "reverted": sp_total_iters > next_b_total_iters, + } + if curr_b.finished_reward_slot_hashes is not None: + assert curr_b.finished_challenge_slot_hashes is not None + for eos_rc in curr_b.finished_challenge_slot_hashes: + if eos_rc == rc_challenge: + if next_b is None: + return {"signage_point": sp, "time_received": time_received, "reverted": False} + next_b_total_iters = next_b.ip_sub_slot_total_iters(self.service.constants) + next_b.ip_iters( + self.service.constants + ) + return { + "signage_point": sp, + "time_received": time_received, + "reverted": sp_total_iters > next_b_total_iters, + } + next_b = curr_b + curr_b_optional = self.service.blockchain.try_block_record(curr_b.prev_hash) + if curr_b_optional is None: + break + curr_b = curr_b_optional + + return {"signage_point": sp, "time_received": time_received, "reverted": True} + async def get_block(self, request: Dict) -> Optional[Dict]: if "header_hash" not in request: raise ValueError("No header_hash in request") @@ -393,6 +488,31 @@ class FullNodeRpcApi: "status": status.name, } + async def get_puzzle_and_solution(self, request: Dict) -> Optional[Dict]: + coin_name: bytes32 = hexstr_to_bytes(request["coin_id"]) + height = request["height"] + coin_record = await self.service.coin_store.get_coin_record(coin_name) + if coin_record is None or not coin_record.spent or coin_record.spent_block_index != height: + raise ValueError(f"Invalid height {height}. coin record {coin_record}") + + header_hash = self.service.blockchain.height_to_hash(height) + block: Optional[FullBlock] = await self.service.block_store.get_full_block(header_hash) + + if block is None or block.transactions_generator is None: + raise ValueError("Invalid block or block generator") + + block_generator: Optional[BlockGenerator] = await self.service.blockchain.get_block_generator(block) + assert block_generator is not None + error, puzzle, solution = get_puzzle_and_solution_for_coin( + block_generator, coin_name, self.service.constants.MAX_BLOCK_COST_CLVM + ) + if error is not None: + raise ValueError(f"Error: {error}") + + puzzle_ser: SerializedProgram = SerializedProgram.from_program(Program.to(puzzle)) + solution_ser: SerializedProgram = SerializedProgram.from_program(Program.to(solution)) + return {"coin_solution": CoinSolution(coin_record.coin, puzzle_ser, solution_ser)} + async def get_additions_and_removals(self, request: Dict) -> Optional[Dict]: if "header_hash" not in request: raise ValueError("No header_hash in request") diff --git a/chia/rpc/full_node_rpc_client.py b/chia/rpc/full_node_rpc_client.py index e5f166fcef6b..604ccc78c5d4 100644 --- a/chia/rpc/full_node_rpc_client.py +++ b/chia/rpc/full_node_rpc_client.py @@ -1,9 +1,12 @@ -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Any from chia.consensus.block_record import BlockRecord +from chia.full_node.signage_point import SignagePoint from chia.rpc.rpc_client import RpcClient from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord +from chia.types.coin_solution import CoinSolution +from chia.types.end_of_slot_bundle import EndOfSubSlotBundle from chia.types.full_block import FullBlock from chia.types.spend_bundle import SpendBundle from chia.types.unfinished_header_block import UnfinishedHeaderBlock @@ -72,6 +75,13 @@ class FullNodeRpcClient(RpcClient): return None return network_space_bytes_estimate["space"] + async def get_coin_record_by_name(self, coin_id: bytes32) -> Optional[CoinRecord]: + try: + response = await self.fetch("get_coin_record_by_name", {"name": coin_id.hex()}) + except Exception: + return None + return CoinRecord.from_json_dict(response["coin_record"]) + async def get_coin_records_by_puzzle_hash( self, puzzle_hash: bytes32, @@ -133,6 +143,13 @@ class FullNodeRpcClient(RpcClient): async def push_tx(self, spend_bundle: SpendBundle): return await self.fetch("push_tx", {"spend_bundle": spend_bundle.to_json_dict()}) + async def get_puzzle_and_solution(self, coin_id: bytes32, height: uint32) -> Optional[CoinRecord]: + try: + response = await self.fetch("get_puzzle_and_solution", {"coin_id": coin_id.hex(), "height": height}) + return CoinSolution.from_json_dict(response["coin_solution"]) + except Exception: + return None + async def get_all_mempool_tx_ids(self) -> List[bytes32]: response = await self.fetch("get_all_mempool_tx_ids", {}) return [bytes32(hexstr_to_bytes(tx_id_hex)) for tx_id_hex in response["tx_ids"]] @@ -150,3 +167,26 @@ class FullNodeRpcClient(RpcClient): return response["mempool_item"] except Exception: return None + + async def get_recent_signage_point_or_eos( + self, sp_hash: Optional[bytes32], challenge_hash: Optional[bytes32] + ) -> Optional[Any]: + try: + if sp_hash is not None: + assert challenge_hash is None + response = await self.fetch("get_recent_signage_point_or_eos", {"sp_hash": sp_hash.hex()}) + return { + "signage_point": SignagePoint.from_json_dict(response["signage_point"]), + "time_received": response["time_received"], + "reverted": response["reverted"], + } + else: + assert challenge_hash is not None + response = await self.fetch("get_recent_signage_point_or_eos", {"challenge_hash": challenge_hash.hex()}) + return { + "eos": EndOfSubSlotBundle.from_json_dict(response["eos"]), + "time_received": response["time_received"], + "reverted": response["reverted"], + } + except Exception: + return None diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 39f8c8f336ec..9a780a38a815 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -9,6 +9,8 @@ from blspy import PrivateKey, G1Element from chia.cmds.init_funcs import check_keys from chia.consensus.block_rewards import calculate_base_farmer_reward +from chia.pools.pool_wallet import PoolWallet +from chia.pools.pool_wallet_info import create_pool_state, FARMING_TO_POOL, PoolWalletInfo, PoolState from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import NodeType, make_msg from chia.simulator.simulator_protocol import FarmNewBlockProtocol @@ -21,6 +23,7 @@ from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict from chia.wallet.cc_wallet.cc_wallet import CCWallet +from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk from chia.wallet.rl_wallet.rl_wallet import RLWallet from chia.wallet.derive_keys import master_sk_to_farmer_sk, master_sk_to_pool_sk from chia.wallet.did_wallet.did_wallet import DIDWallet @@ -70,10 +73,12 @@ class WalletRpcApi: "/get_transactions": self.get_transactions, "/get_next_address": self.get_next_address, "/send_transaction": self.send_transaction, + "/send_transaction_multi": self.send_transaction_multi, "/create_backup": self.create_backup, "/get_transaction_count": self.get_transaction_count, "/get_farmed_amount": self.get_farmed_amount, "/create_signed_transaction": self.create_signed_transaction, + "/delete_unconfirmed_transactions": self.delete_unconfirmed_transactions, # Coloured coins and trading "/cc_set_name": self.cc_set_name, "/cc_get_name": self.cc_get_name, @@ -99,6 +104,11 @@ class WalletRpcApi: "/rl_set_user_info": self.rl_set_user_info, "/send_clawback_transaction:": self.send_clawback_transaction, "/add_rate_limited_funds:": self.add_rate_limited_funds, + # Pool Wallet + "/pw_join_pool": self.pw_join_pool, + "/pw_self_pool": self.pw_self_pool, + "/pw_absorb_rewards": self.pw_absorb_rewards, + "/pw_status": self.pw_status, } async def _state_changed(self, *args) -> List[WsRpcMessage]: @@ -332,10 +342,13 @@ class WalletRpcApi: async def create_new_wallet(self, request: Dict): assert self.service.wallet_state_manager is not None - wallet_state_manager = self.service.wallet_state_manager main_wallet = wallet_state_manager.main_wallet host = request["host"] + if "fee" in request: + fee: uint64 = request["fee"] + else: + fee = uint64(0) if request["wallet_type"] == "cc_wallet": if request["mode"] == "new": async with self.service.wallet_state_manager.lock: @@ -447,6 +460,50 @@ class WalletRpcApi: "backup_dids": did_wallet.did_info.backup_ids, "num_verifications_required": did_wallet.did_info.num_of_backup_ids_needed, } + elif request["wallet_type"] == "pool_wallet": + if request["mode"] == "new": + owner_puzzle_hash: bytes32 = await self.service.wallet_state_manager.main_wallet.get_puzzle_hash(True) + + from chia.pools.pool_wallet_info import initial_pool_state_from_dict + + async with self.service.wallet_state_manager.lock: + last_wallet: Optional[ + WalletInfo + ] = await self.service.wallet_state_manager.user_store.get_last_wallet() + assert last_wallet is not None + + next_id = last_wallet.id + 1 + owner_sk: PrivateKey = master_sk_to_singleton_owner_sk( + self.service.wallet_state_manager.private_key, uint32(next_id) + ) + owner_pk: G1Element = owner_sk.get_g1() + + initial_target_state = initial_pool_state_from_dict( + request["initial_target_state"], owner_pk, owner_puzzle_hash + ) + assert initial_target_state is not None + + try: + delayed_address = None + if "p2_singleton_delayed_ph" in request: + delayed_address = hexstr_to_bytes(request["p2_singleton_delayed_ph"]) + tr, p2_singleton_puzzle_hash, launcher_id = await PoolWallet.create_new_pool_wallet_transaction( + wallet_state_manager, + main_wallet, + initial_target_state, + fee, + request.get("p2_singleton_delay_time", None), + delayed_address, + ) + except Exception as e: + raise ValueError(str(e)) + return { + "transaction": tr, + "launcher_id": launcher_id.hex(), + "p2_singleton_puzzle_hash": p2_singleton_puzzle_hash.hex(), + } + elif request["mode"] == "recovery": + raise ValueError("Need upgraded singleton for on-chain recovery") else: # undefined did_type pass @@ -591,6 +648,46 @@ class WalletRpcApi: "transaction_id": tx.name, } + async def send_transaction_multi(self, request): + assert self.service.wallet_state_manager is not None + + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced before sending transactions") + + if int(time.time()) < self.service.constants.INITIAL_FREEZE_END_TIMESTAMP: + end_date = datetime.fromtimestamp(float(self.service.constants.INITIAL_FREEZE_END_TIMESTAMP)) + raise ValueError(f"No transactions before: {end_date}") + + wallet_id = uint32(request["wallet_id"]) + wallet = self.service.wallet_state_manager.wallets[wallet_id] + + async with self.service.wallet_state_manager.lock: + transaction: TransactionRecord = (await self.create_signed_transaction(request, hold_lock=False))[ + "signed_tx" + ] + await wallet.push_transaction(transaction) + + # Transaction may not have been included in the mempool yet. Use get_transaction to check. + return { + "transaction": transaction, + "transaction_id": transaction.name, + } + + async def delete_unconfirmed_transactions(self, request): + wallet_id = uint32(request["wallet_id"]) + if wallet_id not in self.service.wallet_state_manager.wallets: + raise ValueError(f"Wallet id {wallet_id} does not exist") + async with self.service.wallet_state_manager.lock: + async with self.service.wallet_state_manager.tx_store.db_wrapper.lock: + await self.service.wallet_state_manager.tx_store.db_wrapper.begin_transaction() + await self.service.wallet_state_manager.tx_store.delete_unconfirmed_transactions(wallet_id) + if self.service.wallet_state_manager.wallets[wallet_id].type() == WalletType.POOLING_WALLET.value: + self.service.wallet_state_manager.wallets[wallet_id].target_state = None + await self.service.wallet_state_manager.tx_store.db_wrapper.commit_transaction() + # Update the cache + await self.service.wallet_state_manager.tx_store.rebuild_tx_cache() + return {} + async def get_transaction_count(self, request): wallet_id = int(request["wallet_id"]) count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id) @@ -948,14 +1045,19 @@ class WalletRpcApi: fee_amount = 0 last_height_farmed = 0 for record in tx_records: - height = record.height_farmed(self.service.constants.GENESIS_CHALLENGE) - if height > last_height_farmed: - last_height_farmed = height + if record.wallet_id not in self.service.wallet_state_manager.wallets: + continue if record.type == TransactionType.COINBASE_REWARD: + if self.service.wallet_state_manager.wallets[record.wallet_id].type() == WalletType.POOLING_WALLET: + # Don't add pool rewards for pool wallets. + continue pool_reward_amount += record.amount + height = record.height_farmed(self.service.constants.GENESIS_CHALLENGE) if record.type == TransactionType.FEE_REWARD: fee_amount += record.amount - calculate_base_farmer_reward(height) farmer_reward_amount += calculate_base_farmer_reward(height) + if height > last_height_farmed: + last_height_farmed = height amount += record.amount assert amount == pool_reward_amount + farmer_reward_amount + fee_amount @@ -967,7 +1069,7 @@ class WalletRpcApi: "last_height_farmed": last_height_farmed, } - async def create_signed_transaction(self, request): + async def create_signed_transaction(self, request, hold_lock=True): if "additions" not in request or len(request["additions"]) < 1: raise ValueError("Specify additions list") @@ -996,8 +1098,73 @@ class WalletRpcApi: if "coins" in request and len(request["coins"]) > 0: coins = set([Coin.from_json_dict(coin_json) for coin_json in request["coins"]]) - async with self.service.wallet_state_manager.lock: + if hold_lock: + async with self.service.wallet_state_manager.lock: + signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction( + amount_0, puzzle_hash_0, fee, coins=coins, ignore_max_send_amount=True, primaries=additional_outputs + ) + else: signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction( amount_0, puzzle_hash_0, fee, coins=coins, ignore_max_send_amount=True, primaries=additional_outputs ) return {"signed_tx": signed_tx} + + ########################################################################################## + # Pool Wallet + ########################################################################################## + async def pw_join_pool(self, request): + wallet_id = uint32(request["wallet_id"]) + wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + pool_wallet_info: PoolWalletInfo = await wallet.get_current_state() + owner_pubkey = pool_wallet_info.current.owner_pubkey + target_puzzlehash = None + if "target_puzzlehash" in request: + target_puzzlehash = bytes32(hexstr_to_bytes(request["target_puzzlehash"])) + new_target_state: PoolState = create_pool_state( + FARMING_TO_POOL, + target_puzzlehash, + owner_pubkey, + request["pool_url"], + uint32(request["relative_lock_height"]), + ) + async with self.service.wallet_state_manager.lock: + tx: TransactionRecord = await wallet.join_pool(new_target_state) + return {"transaction": tx} + + async def pw_self_pool(self, request): + # Leaving a pool requires two state transitions. + # First we transition to PoolSingletonState.LEAVING_POOL + # Then we transition to FARMING_TO_POOL or SELF_POOLING + wallet_id = uint32(request["wallet_id"]) + wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + + async with self.service.wallet_state_manager.lock: + tx: TransactionRecord = await wallet.self_pool() + return {"transaction": tx} + + async def pw_absorb_rewards(self, request): + """Perform a sweep of the p2_singleton rewards controlled by the pool wallet singleton""" + if await self.service.wallet_state_manager.synced() is False: + raise ValueError("Wallet needs to be fully synced before collecting rewards") + + wallet_id = uint32(request["wallet_id"]) + wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + fee = uint64(request["fee"]) + + async with self.service.wallet_state_manager.lock: + transaction: TransactionRecord = await wallet.claim_pool_rewards(fee) + state: PoolWalletInfo = await wallet.get_current_state() + return {"state": state.to_json_dict(), "transaction": transaction} + + async def pw_status(self, request): + """Return the complete state of the Pool wallet with id `request["wallet_id"]`""" + wallet_id = uint32(request["wallet_id"]) + wallet: PoolWallet = self.service.wallet_state_manager.wallets[wallet_id] + if wallet.type() != WalletType.POOLING_WALLET.value: + raise ValueError(f"wallet_id {wallet_id} is not a pooling wallet") + state: PoolWalletInfo = await wallet.get_current_state() + unconfirmed_transactions: List[TransactionRecord] = await wallet.get_unconfirmed_transactions() + return { + "state": state.to_json_dict(), + "unconfirmed_transactions": unconfirmed_transactions, + } diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index 6b3e40ee6c20..02758a44ab64 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -1,6 +1,7 @@ from pathlib import Path -from typing import Dict, List +from typing import Dict, List, Optional, Any, Tuple +from chia.pools.pool_wallet_info import PoolWalletInfo from chia.rpc.rpc_client import RpcClient from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 @@ -14,7 +15,7 @@ class WalletRpcClient(RpcClient): Client to Chia RPC, connects to a local wallet. Uses HTTP/JSON, and converts back from JSON into native python objects before returning. All api calls use POST requests. Note that this is not the same as the peer protocol, or wallet protocol (which run Chia's - protocol on top of TCP), it's a separate protocol on top of HTTP thats provides easy access + protocol on top of TCP), it's a separate protocol on top of HTTP that provides easy access to the full node. """ @@ -127,6 +128,30 @@ class WalletRpcClient(RpcClient): ) return TransactionRecord.from_json_dict(res["transaction"]) + async def send_transaction_multi( + self, wallet_id: str, additions: List[Dict], coins: List[Coin] = None, fee: uint64 = uint64(0) + ) -> TransactionRecord: + # Converts bytes to hex for puzzle hashes + additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions] + if coins is not None and len(coins) > 0: + coins_json = [c.to_json_dict() for c in coins] + response: Dict = await self.fetch( + "send_transaction_multi", + {"wallet_id": wallet_id, "additions": additions_hex, "coins": coins_json, "fee": fee}, + ) + else: + response = await self.fetch( + "send_transaction_multi", {"wallet_id": wallet_id, "additions": additions_hex, "fee": fee} + ) + return TransactionRecord.from_json_dict(response["transaction"]) + + async def delete_unconfirmed_transactions(self, wallet_id: str) -> None: + await self.fetch( + "delete_unconfirmed_transactions", + {"wallet_id": wallet_id}, + ) + return None + async def create_backup(self, file_path: Path) -> None: return await self.fetch("create_backup", {"file_path": str(file_path.resolve())}) @@ -135,16 +160,72 @@ class WalletRpcClient(RpcClient): async def create_signed_transaction( self, additions: List[Dict], coins: List[Coin] = None, fee: uint64 = uint64(0) - ) -> Dict: + ) -> TransactionRecord: # Converts bytes to hex for puzzle hashes additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions] if coins is not None and len(coins) > 0: coins_json = [c.to_json_dict() for c in coins] - return await self.fetch( + response: Dict = await self.fetch( "create_signed_transaction", {"additions": additions_hex, "coins": coins_json, "fee": fee} ) else: - return await self.fetch("create_signed_transaction", {"additions": additions_hex, "fee": fee}) + response = await self.fetch("create_signed_transaction", {"additions": additions_hex, "fee": fee}) + return TransactionRecord.from_json_dict(response["signed_tx"]) + async def create_new_pool_wallet( + self, + target_puzzlehash: Optional[bytes32], + pool_url: Optional[str], + relative_lock_height: uint32, + backup_host: str, + mode: str, + state: str, + p2_singleton_delay_time: Optional[uint64] = None, + p2_singleton_delayed_ph: Optional[bytes32] = None, + ) -> TransactionRecord: -# TODO: add APIs for coloured coins and RL wallet + request: Dict[str, Any] = { + "wallet_type": "pool_wallet", + "mode": mode, + "host": backup_host, + "initial_target_state": { + "target_puzzle_hash": target_puzzlehash.hex() if target_puzzlehash else None, + "relative_lock_height": relative_lock_height, + "pool_url": pool_url, + "state": state, + }, + } + if p2_singleton_delay_time is not None: + request["p2_singleton_delay_time"] = p2_singleton_delay_time + if p2_singleton_delayed_ph is not None: + request["p2_singleton_delayed_ph"] = p2_singleton_delayed_ph.hex() + res = await self.fetch("create_new_wallet", request) + return TransactionRecord.from_json_dict(res["transaction"]) + + async def pw_self_pool(self, wallet_id: str) -> TransactionRecord: + return TransactionRecord.from_json_dict( + (await self.fetch("pw_self_pool", {"wallet_id": wallet_id}))["transaction"] + ) + + async def pw_join_pool( + self, wallet_id: str, target_puzzlehash: bytes32, pool_url: str, relative_lock_height: uint32 + ) -> TransactionRecord: + request = { + "wallet_id": int(wallet_id), + "target_puzzlehash": target_puzzlehash.hex(), + "relative_lock_height": relative_lock_height, + "pool_url": pool_url, + } + return TransactionRecord.from_json_dict((await self.fetch("pw_join_pool", request))["transaction"]) + + async def pw_absorb_rewards(self, wallet_id: str, fee: uint64 = uint64(0)) -> TransactionRecord: + return TransactionRecord.from_json_dict( + (await self.fetch("pw_absorb_rewards", {"wallet_id": wallet_id, "fee": fee}))["transaction"] + ) + + async def pw_status(self, wallet_id: str) -> Tuple[PoolWalletInfo, List[TransactionRecord]]: + json_dict = await self.fetch("pw_status", {"wallet_id": wallet_id}) + return ( + PoolWalletInfo.from_json_dict(json_dict["state"]), + [TransactionRecord.from_json_dict(tr) for tr in json_dict["unconfirmed_transactions"]], + ) diff --git a/chia/server/node_discovery.py b/chia/server/node_discovery.py index 4f2ae3b62182..d18e793a08f6 100644 --- a/chia/server/node_discovery.py +++ b/chia/server/node_discovery.py @@ -79,7 +79,7 @@ class FullNodeDiscovery: self.cleanup_task: Optional[asyncio.Task] = None self.initial_wait: int = 0 try: - self.resolver = dns.asyncresolver.Resolver() + self.resolver: Optional[dns.asyncresolver.Resolver] = dns.asyncresolver.Resolver() except Exception: self.resolver = None self.log.exception("Error initializing asyncresolver") diff --git a/chia/server/rate_limits.py b/chia/server/rate_limits.py index c0f7a72861a4..7a6f39c64e15 100644 --- a/chia/server/rate_limits.py +++ b/chia/server/rate_limits.py @@ -95,6 +95,8 @@ rate_limits_other = { ProtocolMessageTypes.request_peers_introducer: RLSettings(100, 100), ProtocolMessageTypes.respond_peers_introducer: RLSettings(100, 1024 * 1024), ProtocolMessageTypes.farm_new_block: RLSettings(200, 200), + ProtocolMessageTypes.request_plots: RLSettings(10, 10 * 1024 * 1024), + ProtocolMessageTypes.respond_plots: RLSettings(10, 10 * 1024 * 1024), } diff --git a/chia/types/blockchain_format/coin.py b/chia/types/blockchain_format/coin.py index 15c9c22922cb..456980fb9eb3 100644 --- a/chia/types/blockchain_format/coin.py +++ b/chia/types/blockchain_format/coin.py @@ -15,7 +15,7 @@ class Coin(Streamable): This structure is used in the body for the reward and fees genesis coins. """ - parent_coin_info: bytes32 + parent_coin_info: bytes32 # down with this sort of thing. puzzle_hash: bytes32 amount: uint64 diff --git a/chia/types/blockchain_format/program.py b/chia/types/blockchain_format/program.py index 74af58e2a9b6..d3be6bb2d1c2 100644 --- a/chia/types/blockchain_format/program.py +++ b/chia/types/blockchain_format/program.py @@ -1,5 +1,5 @@ import io -from typing import List, Optional, Set, Tuple +from typing import List, Set, Tuple from clvm import KEYWORD_FROM_ATOM, KEYWORD_TO_ATOM, SExp from clvm import run_program as default_run_program @@ -54,6 +54,9 @@ class Program(SExp): assert f.read() == b"" return result + def to_serialized_program(self) -> "SerializedProgram": + return SerializedProgram.from_bytes(bytes(self)) + def __bytes__(self) -> bytes: f = io.BytesIO() self.stream(f) # type: ignore # noqa @@ -82,8 +85,11 @@ class Program(SExp): cost, r = curry(self, list(args)) return Program.to(r) - def uncurry(self) -> Optional[Tuple["Program", "Program"]]: - return uncurry(self) + def uncurry(self) -> Tuple["Program", "Program"]: + r = uncurry(self) + if r is None: + return self, self.to(0) + return r def as_int(self) -> int: return int_from_bytes(self.as_atom()) @@ -160,6 +166,18 @@ class SerializedProgram: ret._buf = bytes(blob) return ret + @classmethod + def from_program(cls, p: Program) -> "SerializedProgram": + ret = SerializedProgram() + ret._buf = bytes(p) + return ret + + def to_program(self) -> Program: + return Program.from_bytes(self._buf) + + def uncurry(self) -> Tuple["Program", "Program"]: + return self.to_program().uncurry() + def __bytes__(self) -> bytes: return self._buf diff --git a/chia/types/blockchain_format/proof_of_space.py b/chia/types/blockchain_format/proof_of_space.py index ed90467d8fd6..75ca7443da0a 100644 --- a/chia/types/blockchain_format/proof_of_space.py +++ b/chia/types/blockchain_format/proof_of_space.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from typing import Optional from bitstring import BitArray -from blspy import G1Element +from blspy import G1Element, AugSchemeMPL, PrivateKey from chiapos import Verifier from chia.consensus.constants import ConsensusConstants @@ -39,20 +39,26 @@ class ProofOfSpace(Streamable): ) -> Optional[bytes32]: # Exactly one of (pool_public_key, pool_contract_puzzle_hash) must not be None if (self.pool_public_key is None) and (self.pool_contract_puzzle_hash is None): + log.error("Fail 1") return None if (self.pool_public_key is not None) and (self.pool_contract_puzzle_hash is not None): + log.error("Fail 2") return None if self.size < constants.MIN_PLOT_SIZE: + log.error("Fail 3") return None if self.size > constants.MAX_PLOT_SIZE: + log.error("Fail 4") return None plot_id: bytes32 = self.get_plot_id() new_challenge: bytes32 = ProofOfSpace.calculate_pos_challenge(plot_id, original_challenge_hash, signage_point) if new_challenge != self.challenge: + log.error("New challenge is not challenge") return None if not ProofOfSpace.passes_plot_filter(constants, plot_id, original_challenge_hash, signage_point): + log.error("Fail 5") return None return self.get_quality_string(plot_id) @@ -98,5 +104,15 @@ class ProofOfSpace(Streamable): return std_hash(bytes(pool_contract_puzzle_hash) + bytes(plot_public_key)) @staticmethod - def generate_plot_public_key(local_pk: G1Element, farmer_pk: G1Element) -> G1Element: - return local_pk + farmer_pk + def generate_taproot_sk(local_pk: G1Element, farmer_pk: G1Element) -> PrivateKey: + taproot_message: bytes = bytes(local_pk + farmer_pk) + bytes(local_pk) + bytes(farmer_pk) + taproot_hash: bytes32 = std_hash(taproot_message) + return AugSchemeMPL.key_gen(taproot_hash) + + @staticmethod + def generate_plot_public_key(local_pk: G1Element, farmer_pk: G1Element, include_taproot: bool = False) -> G1Element: + if include_taproot: + taproot_sk: PrivateKey = ProofOfSpace.generate_taproot_sk(local_pk, farmer_pk) + return local_pk + farmer_pk + taproot_sk.get_g1() + else: + return local_pk + farmer_pk diff --git a/chia/types/blockchain_format/vdf.py b/chia/types/blockchain_format/vdf.py index 7d1d902da54e..4d417502cc70 100644 --- a/chia/types/blockchain_format/vdf.py +++ b/chia/types/blockchain_format/vdf.py @@ -16,7 +16,7 @@ from chia.util.streamable import Streamable, streamable log = logging.getLogger(__name__) -@lru_cache(maxsize=20) +@lru_cache(maxsize=200) def get_discriminant(challenge, size_bites) -> int: return int( create_discriminant(challenge, size_bites), @@ -24,7 +24,7 @@ def get_discriminant(challenge, size_bites) -> int: ) -@lru_cache(maxsize=100) +@lru_cache(maxsize=1000) def verify_vdf( disc: int, input_el: bytes100, diff --git a/chia/types/spend_bundle.py b/chia/types/spend_bundle.py index ad9e159c785f..5effc214cfc7 100644 --- a/chia/types/spend_bundle.py +++ b/chia/types/spend_bundle.py @@ -6,6 +6,7 @@ from blspy import AugSchemeMPL, G2Element from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.streamable import Streamable, streamable +from chia.wallet.util.debug_spend_bundle import debug_spend_bundle from .coin_solution import CoinSolution @@ -53,6 +54,9 @@ class SpendBundle(Streamable): def name(self) -> bytes32: return self.get_hash() + def debug(self, agg_sig_additional_data=bytes([3] * 32)): + debug_spend_bundle(self, agg_sig_additional_data) + def not_ephemeral_additions(self): all_removals = self.removals() all_additions = self.additions() diff --git a/chia/util/config.py b/chia/util/config.py index 870ac7dd02d2..25e8dad17377 100644 --- a/chia/util/config.py +++ b/chia/util/config.py @@ -15,8 +15,8 @@ def initial_config_file(filename: Union[str, Path]) -> str: return pkg_resources.resource_string(__name__, f"initial-{filename}").decode() -def create_default_chia_config(root_path: Path) -> None: - for filename in ["config.yaml"]: +def create_default_chia_config(root_path: Path, filenames=["config.yaml"]) -> None: + for filename in filenames: default_config_file_data = initial_config_file(filename) path = config_path_for_filename(root_path, filename) mkdir(path.parent) diff --git a/chia/wallet/cc_wallet/cc_wallet.py b/chia/wallet/cc_wallet/cc_wallet.py index 497ec77d6197..4ea0df2dd5f8 100644 --- a/chia/wallet/cc_wallet/cc_wallet.py +++ b/chia/wallet/cc_wallet/cc_wallet.py @@ -283,7 +283,7 @@ class CCWallet: assert self.cc_info.my_genesis_checker is not None return bytes(self.cc_info.my_genesis_checker).hex() - async def coin_added(self, coin: Coin, header_hash: bytes32, removals: List[Coin], height: uint32): + async def coin_added(self, coin: Coin, height: uint32): """Notification from wallet state manager that wallet has been received.""" self.log.info(f"CC wallet has been notified that {coin} was added") diff --git a/chia/wallet/derive_keys.py b/chia/wallet/derive_keys.py index ca7cbf4a60b4..52747be0c1a1 100644 --- a/chia/wallet/derive_keys.py +++ b/chia/wallet/derive_keys.py @@ -1,6 +1,6 @@ -from typing import List +from typing import List, Optional -from blspy import AugSchemeMPL, PrivateKey +from blspy import AugSchemeMPL, PrivateKey, G1Element from chia.util.ints import uint32 @@ -8,7 +8,7 @@ from chia.util.ints import uint32 # https://eips.ethereum.org/EIPS/eip-2334 # 12381 = bls spec number # 8444 = Chia blockchain number and port number -# 0, 1, 2, 3, 4, farmer, pool, wallet, local, backup key numbers +# 0, 1, 2, 3, 4, 5, 6 farmer, pool, wallet, local, backup key, singleton, pooling authentication key numbers def _derive_path(sk: PrivateKey, path: List[int]) -> PrivateKey: @@ -35,3 +35,40 @@ def master_sk_to_local_sk(master: PrivateKey) -> PrivateKey: def master_sk_to_backup_sk(master: PrivateKey) -> PrivateKey: return _derive_path(master, [12381, 8444, 4, 0]) + + +def master_sk_to_singleton_owner_sk(master: PrivateKey, wallet_id: uint32) -> PrivateKey: + """ + This key controls a singleton on the blockchain, allowing for dynamic pooling (changing pools) + """ + return _derive_path(master, [12381, 8444, 5, wallet_id]) + + +def master_sk_to_pooling_authentication_sk(master: PrivateKey, wallet_id: uint32, index: uint32) -> PrivateKey: + """ + This key is used for the farmer to authenticate to the pool when sending partials + """ + assert index < 10000 + assert wallet_id < 10000 + return _derive_path(master, [12381, 8444, 6, wallet_id * 10000 + index]) + + +async def find_owner_sk(all_sks: List[PrivateKey], owner_pk: G1Element) -> Optional[G1Element]: + for wallet_id in range(50): + for sk in all_sks: + auth_sk = master_sk_to_singleton_owner_sk(sk, uint32(wallet_id)) + if auth_sk.get_g1() == owner_pk: + return auth_sk + return None + + +async def find_authentication_sk(all_sks: List[PrivateKey], authentication_pk: G1Element) -> Optional[PrivateKey]: + # NOTE: might need to increase this if using a large number of wallets, or have switched authentication keys + # many times. + for auth_key_index in range(20): + for wallet_id in range(20): + for sk in all_sks: + auth_sk = master_sk_to_pooling_authentication_sk(sk, uint32(wallet_id), uint32(auth_key_index)) + if auth_sk.get_g1() == authentication_pk: + return auth_sk + return None diff --git a/chia/wallet/did_wallet/did_info.py b/chia/wallet/did_wallet/did_info.py index 0fd299344e8b..fd32ee1429ec 100644 --- a/chia/wallet/did_wallet/did_info.py +++ b/chia/wallet/did_wallet/did_info.py @@ -4,7 +4,7 @@ from typing import List, Optional, Tuple from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.ints import uint64 from chia.util.streamable import streamable, Streamable -from chia.wallet.cc_wallet.ccparent import CCParent +from chia.wallet.lineage_proof import LineageProof from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.coin import Coin @@ -15,7 +15,7 @@ class DIDInfo(Streamable): origin_coin: Optional[Coin] # puzzlehash of this coin is our DID backup_ids: List[bytes] num_of_backup_ids_needed: uint64 - parent_info: List[Tuple[bytes32, Optional[CCParent]]] # {coin.name(): CCParent} + parent_info: List[Tuple[bytes32, Optional[LineageProof]]] # {coin.name(): LineageProof} current_inner: Optional[Program] # represents a Program as bytes temp_coin: Optional[Coin] # partially recovered wallet uses these to hold info temp_puzhash: Optional[bytes32] diff --git a/chia/wallet/did_wallet/did_wallet.py b/chia/wallet/did_wallet/did_wallet.py index 8a65268bb794..42dd355f9d7d 100644 --- a/chia/wallet/did_wallet/did_wallet.py +++ b/chia/wallet/did_wallet/did_wallet.py @@ -11,7 +11,7 @@ from chia.protocols.wallet_protocol import RespondAdditions, RejectAdditionsRequ from chia.server.outbound_message import NodeType from chia.types.blockchain_format.coin import Coin from chia.types.coin_solution import CoinSolution - +from chia.types.announcement import Announcement from chia.types.blockchain_format.program import Program from chia.types.spend_bundle import SpendBundle from chia.types.blockchain_format.sized_bytes import bytes32 @@ -19,7 +19,7 @@ from chia.wallet.util.transaction_type import TransactionType from chia.util.ints import uint64, uint32, uint8 from chia.wallet.did_wallet.did_info import DIDInfo -from chia.wallet.cc_wallet.ccparent import CCParent +from chia.wallet.lineage_proof import LineageProof from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet import Wallet @@ -83,7 +83,7 @@ class DIDWallet: await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id) assert self.did_info.origin_coin is not None did_puzzle_hash = did_wallet_puzzles.create_fullpuz( - self.did_info.current_inner, self.did_info.origin_coin.puzzle_hash + self.did_info.current_inner, self.did_info.origin_coin.name() ).get_tree_hash() did_record = TransactionRecord( @@ -275,7 +275,7 @@ class DIDWallet: return used_coins # This will be used in the recovery case where we don't have the parent info already - async def coin_added(self, coin: Coin, header_hash: bytes32, removals: List[Coin], height: int): + async def coin_added(self, coin: Coin, _: uint32): """Notification from wallet state manager that wallet has been received.""" self.log.info("DID wallet has been notified that coin was added") inner_puzzle = await self.inner_puzzle_for_did_puzzle(coin.puzzle_hash) @@ -291,7 +291,7 @@ class DIDWallet: ) await self.save_info(new_info, True) - future_parent = CCParent( + future_parent = LineageProof( coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount, @@ -345,7 +345,7 @@ class DIDWallet: await self.save_info(did_info, False) await self.wallet_state_manager.update_wallet_puzzle_hashes(self.wallet_info.id) - full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.puzzle_hash) + full_puz = did_wallet_puzzles.create_fullpuz(innerpuz, origin.name()) full_puzzle_hash = full_puz.get_tree_hash() ( sub_height, @@ -381,7 +381,7 @@ class DIDWallet: if puzzle_hash == full_puzzle_hash: # our coin for coin in coins: - future_parent = CCParent( + future_parent = LineageProof( coin.parent_coin_info, innerpuz.get_tree_hash(), coin.amount, @@ -410,7 +410,7 @@ class DIDWallet: pubkey, self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed ) if self.did_info.origin_coin is not None: - return did_wallet_puzzles.create_fullpuz(innerpuz, self.did_info.origin_coin.puzzle_hash) + return did_wallet_puzzles.create_fullpuz(innerpuz, self.did_info.origin_coin.name()) else: return did_wallet_puzzles.create_fullpuz(innerpuz, 0x00) @@ -421,31 +421,30 @@ class DIDWallet: def get_my_DID(self) -> str: assert self.did_info.origin_coin is not None - core = self.did_info.origin_coin.puzzle_hash + core = self.did_info.origin_coin.name() assert core is not None return core.hex() - # This is used to cash out, or update the id_list - async def create_spend(self, puzhash: bytes32): + async def create_update_spend(self): assert self.did_info.current_inner is not None assert self.did_info.origin_coin is not None coins = await self.select_coins(1) assert coins is not None coin = coins.pop() - # innerpuz solution is (mode amount new_puz identity my_puz) - innersol: Program = Program.to([0, coin.amount, puzhash, coin.name(), coin.puzzle_hash]) + new_puzhash = await self.get_new_inner_hash() + # innerpuz solution is (mode amount messages new_puz) + innersol: Program = Program.to([1, coin.amount, [], new_puzhash]) # full solution is (corehash parent_info my_amount innerpuz_reveal solution) innerpuz: Program = self.did_info.current_inner full_puzzle: Program = did_wallet_puzzles.create_fullpuz( innerpuz, - self.did_info.origin_coin.puzzle_hash, + self.did_info.origin_coin.name(), ) parent_info = await self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ - [self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount], [ parent_info.parent_name, parent_info.inner_puzzle_hash, @@ -458,7 +457,141 @@ class DIDWallet: list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)] # sign for AGG_SIG_ME message = ( - Program.to([coin.amount, puzhash]).get_tree_hash() + Program.to([new_puzhash, coin.amount, []]).get_tree_hash() + + coin.name() + + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ) + pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) + index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) + private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + signature = AugSchemeMPL.sign(private, message) + # assert signature.validate([signature.PkMessagePair(pubkey, message)]) + sigs = [signature] + aggsig = AugSchemeMPL.aggregate(sigs) + spend_bundle = SpendBundle(list_of_solutions, aggsig) + + did_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=new_puzhash, + amount=uint64(coin.amount), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=spend_bundle.additions(), + removals=spend_bundle.removals(), + wallet_id=self.wallet_info.id, + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=token_bytes(), + ) + await self.standard_wallet.push_transaction(did_record) + return spend_bundle + + # The message spend can send messages and also change your innerpuz + async def create_message_spend(self, messages: List[bytes], new_innerpuzhash: Optional[bytes32] = None): + assert self.did_info.current_inner is not None + assert self.did_info.origin_coin is not None + coins = await self.select_coins(1) + assert coins is not None + coin = coins.pop() + innerpuz: Program = self.did_info.current_inner + if new_innerpuzhash is None: + new_innerpuzhash = innerpuz.get_tree_hash() + # innerpuz solution is (mode amount messages new_puz) + innersol: Program = Program.to([1, coin.amount, messages, new_innerpuzhash]) + # full solution is (corehash parent_info my_amount innerpuz_reveal solution) + + full_puzzle: Program = did_wallet_puzzles.create_fullpuz( + innerpuz, + self.did_info.origin_coin.name(), + ) + parent_info = await self.get_parent_for_coin(coin) + assert parent_info is not None + fullsol = Program.to( + [ + [ + parent_info.parent_name, + parent_info.inner_puzzle_hash, + parent_info.amount, + ], + coin.amount, + innersol, + ] + ) + list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)] + # sign for AGG_SIG_ME + # new_inner_puzhash amount message + message = ( + Program.to([new_innerpuzhash, coin.amount, messages]).get_tree_hash() + + coin.name() + + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA + ) + pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) + index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) + private = master_sk_to_wallet_sk(self.wallet_state_manager.private_key, index) + signature = AugSchemeMPL.sign(private, message) + # assert signature.validate([signature.PkMessagePair(pubkey, message)]) + sigs = [signature] + aggsig = AugSchemeMPL.aggregate(sigs) + spend_bundle = SpendBundle(list_of_solutions, aggsig) + + did_record = TransactionRecord( + confirmed_at_height=uint32(0), + created_at_time=uint64(int(time.time())), + to_puzzle_hash=new_innerpuzhash, + amount=uint64(coin.amount), + fee_amount=uint64(0), + confirmed=False, + sent=uint32(0), + spend_bundle=spend_bundle, + additions=spend_bundle.additions(), + removals=spend_bundle.removals(), + wallet_id=self.wallet_info.id, + sent_to=[], + trade_id=None, + type=uint32(TransactionType.OUTGOING_TX.value), + name=token_bytes(), + ) + await self.standard_wallet.push_transaction(did_record) + return spend_bundle + + # This is used to cash out, or update the id_list + async def create_exit_spend(self, puzhash: bytes32): + assert self.did_info.current_inner is not None + assert self.did_info.origin_coin is not None + coins = await self.select_coins(1) + assert coins is not None + coin = coins.pop() + amount = coin.amount - 1 + # innerpuz solution is (mode amount new_puzhash) + innersol: Program = Program.to([0, amount, puzhash]) + # full solution is (corehash parent_info my_amount innerpuz_reveal solution) + innerpuz: Program = self.did_info.current_inner + + full_puzzle: Program = did_wallet_puzzles.create_fullpuz( + innerpuz, + self.did_info.origin_coin.name(), + ) + parent_info = await self.get_parent_for_coin(coin) + assert parent_info is not None + fullsol = Program.to( + [ + [ + parent_info.parent_name, + parent_info.inner_puzzle_hash, + parent_info.amount, + ], + coin.amount, + innersol, + ] + ) + list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)] + # sign for AGG_SIG_ME + message = ( + Program.to([amount, puzhash]).get_tree_hash() + coin.name() + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA ) @@ -503,20 +636,20 @@ class DIDWallet: coin = coins.pop() message = did_wallet_puzzles.create_recovery_message_puzzle(recovering_coin_name, newpuz, pubkey) innermessage = message.get_tree_hash() - # innerpuz solution is (mode amount new_puz identity my_puz) - innersol = Program.to([1, coin.amount, innermessage, recovering_coin_name, coin.puzzle_hash]) - # full solution is (corehash parent_info my_amount innerpuz_reveal solution) innerpuz: Program = self.did_info.current_inner + # innerpuz solution is (mode, amount, message, new_inner_puzhash) + innersol = Program.to([1, coin.amount, [innermessage], innerpuz.get_tree_hash()]) + + # full solution is (corehash parent_info my_amount innerpuz_reveal solution) full_puzzle: Program = did_wallet_puzzles.create_fullpuz( innerpuz, - self.did_info.origin_coin.puzzle_hash, + self.did_info.origin_coin.name(), ) parent_info = await self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ - [self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount], [ parent_info.parent_name, parent_info.inner_puzzle_hash, @@ -528,10 +661,9 @@ class DIDWallet: ) list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)] message_spend = did_wallet_puzzles.create_spend_for_message(coin.name(), recovering_coin_name, newpuz, pubkey) - message_spend_bundle = SpendBundle([message_spend], AugSchemeMPL.aggregate([])) # sign for AGG_SIG_ME - to_sign = Program.to([coin.puzzle_hash, coin.amount, innermessage]).get_tree_hash() + to_sign = Program.to([innerpuz.get_tree_hash(), coin.amount, [innermessage]]).get_tree_hash() message = to_sign + coin.name() + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA pubkey = did_wallet_puzzles.get_pubkey_from_innerpuz(innerpuz) index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey) @@ -629,31 +761,29 @@ class DIDWallet: spend_bundle: SpendBundle, ) -> SpendBundle: assert self.did_info.origin_coin is not None - # innerpuz solution is (mode amount new_puz identity my_puz parent_innerpuzhash_amounts_for_recovery_ids) + + # innersol is (mode amount new_puz my_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey recovery_list_reveal) # noqa innersol = Program.to( [ 2, coin.amount, puzhash, - coin.name(), - coin.puzzle_hash, + puzhash, parent_innerpuzhash_amounts_for_recovery_ids, bytes(pubkey), self.did_info.backup_ids, - self.did_info.num_of_backup_ids_needed, ] ) # full solution is (parent_info my_amount solution) innerpuz = self.did_info.current_inner full_puzzle: Program = did_wallet_puzzles.create_fullpuz( innerpuz, - self.did_info.origin_coin.puzzle_hash, + self.did_info.origin_coin.name(), ) parent_info = await self.get_parent_for_coin(coin) assert parent_info is not None fullsol = Program.to( [ - [self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount], [ parent_info.parent_name, parent_info.inner_puzzle_hash, @@ -734,7 +864,7 @@ class DIDWallet: ) return inner_puzzle - async def get_parent_for_coin(self, coin) -> Optional[CCParent]: + async def get_parent_for_coin(self, coin) -> Optional[LineageProof]: parent_info = None for name, ccparent in self.did_info.parent_info: if name == coin.parent_coin_info: @@ -752,25 +882,36 @@ class DIDWallet: return None origin = coins.copy().pop() + genesis_launcher_puz = did_wallet_puzzles.SINGLETON_LAUNCHER + launcher_coin = Coin(origin.name(), genesis_launcher_puz.get_tree_hash(), amount) did_inner: Program = await self.get_new_innerpuz() did_inner_hash = did_inner.get_tree_hash() - did_puz = did_wallet_puzzles.create_fullpuz(did_inner, origin.puzzle_hash) - did_puzzle_hash = did_puz.get_tree_hash() + did_full_puz = did_wallet_puzzles.create_fullpuz(did_inner, launcher_coin.name()) + did_puzzle_hash = did_full_puz.get_tree_hash() + + announcement_set: Set[Announcement] = set() + announcement_message = Program.to([did_puzzle_hash, amount, bytes(0x80)]).get_tree_hash() + announcement_set.add(Announcement(launcher_coin.name(), announcement_message).name()) tx_record: Optional[TransactionRecord] = await self.standard_wallet.generate_signed_transaction( - amount, did_puzzle_hash, uint64(0), origin.name(), coins + amount, genesis_launcher_puz.get_tree_hash(), uint64(0), origin.name(), coins, None, False, announcement_set ) - eve_coin = Coin(origin.name(), did_puzzle_hash, amount) - future_parent = CCParent( + + genesis_launcher_solution = Program.to([did_puzzle_hash, amount, bytes(0x80)]) + + launcher_cs = CoinSolution(launcher_coin, genesis_launcher_puz, genesis_launcher_solution) + launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([])) + eve_coin = Coin(launcher_coin.name(), did_puzzle_hash, amount) + future_parent = LineageProof( eve_coin.parent_coin_info, did_inner_hash, eve_coin.amount, ) - eve_parent = CCParent( - origin.parent_coin_info, - origin.puzzle_hash, - origin.amount, + eve_parent = LineageProof( + launcher_coin.parent_coin_info, + launcher_coin.puzzle_hash, + launcher_coin.amount, ) await self.add_parent(eve_coin.parent_coin_info, eve_parent, False) await self.add_parent(eve_coin.name(), future_parent, False) @@ -780,7 +921,7 @@ class DIDWallet: # Only want to save this information if the transaction is valid did_info: DIDInfo = DIDInfo( - origin, + launcher_coin, self.did_info.backup_ids, self.did_info.num_of_backup_ids_needed, self.did_info.parent_info, @@ -790,19 +931,18 @@ class DIDWallet: None, ) await self.save_info(did_info, False) - eve_spend = await self.generate_eve_spend(eve_coin, did_puz, did_inner) - full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend]) + eve_spend = await self.generate_eve_spend(eve_coin, did_full_puz, did_inner) + full_spend = SpendBundle.aggregate([tx_record.spend_bundle, eve_spend, launcher_sb]) return full_spend async def generate_eve_spend(self, coin: Coin, full_puzzle: Program, innerpuz: Program): assert self.did_info.origin_coin is not None - # innerpuz solution is (mode amount message my_id my_puzhash parent_innerpuzhash_amounts_for_recovery_ids) - innersol = Program.to([0, coin.amount, coin.puzzle_hash, coin.name(), coin.puzzle_hash, []]) + # innerpuz solution is (mode amount message new_puzhash) + innersol = Program.to([1, coin.amount, [], innerpuz.get_tree_hash()]) # full solution is (parent_info my_amount innersolution) fullsol = Program.to( [ [self.did_info.origin_coin.parent_coin_info, self.did_info.origin_coin.amount], - coin.parent_coin_info, coin.amount, innersol, ] @@ -810,7 +950,7 @@ class DIDWallet: list_of_solutions = [CoinSolution(coin, full_puzzle, fullsol)] # sign for AGG_SIG_ME message = ( - Program.to([coin.amount, coin.puzzle_hash]).get_tree_hash() + Program.to([innerpuz.get_tree_hash(), coin.amount, []]).get_tree_hash() + coin.name() + self.wallet_state_manager.constants.AGG_SIG_ME_ADDITIONAL_DATA ) @@ -837,7 +977,7 @@ class DIDWallet: return max_send_amount - async def add_parent(self, name: bytes32, parent: Optional[CCParent], in_transaction: bool): + async def add_parent(self, name: bytes32, parent: Optional[LineageProof], in_transaction: bool): self.log.info(f"Adding parent {name}: {parent}") current_list = self.did_info.parent_info.copy() current_list.append((name, parent)) diff --git a/chia/wallet/did_wallet/did_wallet_puzzles.py b/chia/wallet/did_wallet/did_wallet_puzzles.py index deb8572ee206..e8c69bdcba82 100644 --- a/chia/wallet/did_wallet/did_wallet_puzzles.py +++ b/chia/wallet/did_wallet/did_wallet_puzzles.py @@ -9,18 +9,22 @@ from chia.util.ints import uint64 from chia.wallet.puzzles.load_clvm import load_clvm from chia.types.condition_opcodes import ConditionOpcode -DID_CORE_MOD = load_clvm("singleton_top_layer.clvm") + +SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer.clvm") +LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") DID_INNERPUZ_MOD = load_clvm("did_innerpuz.clvm") +SINGLETON_LAUNCHER = load_clvm("singleton_launcher.clvm") def create_innerpuz(pubkey: bytes, identities: List[bytes], num_of_backup_ids_needed: uint64) -> Program: backup_ids_hash = Program(Program.to(identities)).get_tree_hash() - return DID_INNERPUZ_MOD.curry(DID_CORE_MOD.get_tree_hash(), pubkey, backup_ids_hash, num_of_backup_ids_needed) + # MOD_HASH MY_PUBKEY RECOVERY_DID_LIST_HASH NUM_VERIFICATIONS_REQUIRED + return DID_INNERPUZ_MOD.curry(pubkey, backup_ids_hash, num_of_backup_ids_needed) -def create_fullpuz(innerpuz, genesis_puzhash) -> Program: - mod_hash = DID_CORE_MOD.get_tree_hash() - return DID_CORE_MOD.curry(mod_hash, genesis_puzhash, innerpuz) +def create_fullpuz(innerpuz, genesis_id) -> Program: + mod_hash = SINGLETON_TOP_LAYER_MOD.get_tree_hash() + return SINGLETON_TOP_LAYER_MOD.curry(mod_hash, genesis_id, LAUNCHER_PUZZLE.get_tree_hash(), innerpuz) def get_pubkey_from_innerpuz(innerpuz: Program) -> G1Element: @@ -41,7 +45,7 @@ def is_did_innerpuz(inner_f: Program): def is_did_core(inner_f: Program): - return inner_f == DID_CORE_MOD + return inner_f == SINGLETON_TOP_LAYER_MOD def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program]]: @@ -56,7 +60,7 @@ def uncurry_innerpuz(puzzle: Program) -> Optional[Tuple[Program, Program]]: if not is_did_innerpuz(inner_f): return None - core_mod, pubkey, id_list, num_of_backup_ids_needed = list(args.as_iter()) + pubkey, id_list, num_of_backup_ids_needed = list(args.as_iter()) return pubkey, id_list @@ -71,8 +75,8 @@ def get_innerpuzzle_from_puzzle(puzzle: Program) -> Optional[Program]: return inner_puzzle -def create_recovery_message_puzzle(recovering_coin: bytes32, newpuz: bytes32, pubkey: G1Element): - puzstring = f"(q . ((0x{ConditionOpcode.CREATE_COIN_ANNOUNCEMENT.hex()} 0x{recovering_coin.hex()}) (0x{ConditionOpcode.AGG_SIG_UNSAFE.hex()} 0x{bytes(pubkey).hex()} 0x{newpuz.hex()})))" # noqa +def create_recovery_message_puzzle(recovering_coin_id: bytes32, newpuz: bytes32, pubkey: G1Element): + puzstring = f"(q . ((0x{ConditionOpcode.CREATE_COIN_ANNOUNCEMENT.hex()} 0x{recovering_coin_id.hex()}) (0x{ConditionOpcode.AGG_SIG_UNSAFE.hex()} 0x{bytes(pubkey).hex()} 0x{newpuz.hex()})))" # noqa puz = binutils.assemble(puzstring) return Program.to(puz) diff --git a/chia/wallet/cc_wallet/ccparent.py b/chia/wallet/lineage_proof.py similarity index 91% rename from chia/wallet/cc_wallet/ccparent.py rename to chia/wallet/lineage_proof.py index 3ce7a9a1c9f3..0c3f04030be5 100644 --- a/chia/wallet/cc_wallet/ccparent.py +++ b/chia/wallet/lineage_proof.py @@ -8,7 +8,7 @@ from chia.util.streamable import Streamable, streamable @dataclass(frozen=True) @streamable -class CCParent(Streamable): +class LineageProof(Streamable): parent_name: bytes32 inner_puzzle_hash: Optional[bytes32] amount: uint64 diff --git a/chia/wallet/puzzles/curry-and-treehash.clinc b/chia/wallet/puzzles/curry-and-treehash.clinc new file mode 100644 index 000000000000..0adce2d7a063 --- /dev/null +++ b/chia/wallet/puzzles/curry-and-treehash.clinc @@ -0,0 +1,68 @@ +( + ;; The code below is used to calculate of the tree hash of a curried function + ;; without actually doing the curry, and using other optimization tricks + ;; like unrolling `sha256tree`. + + (defconstant ONE 1) + (defconstant TWO 2) + (defconstant A_KW #a) + (defconstant Q_KW #q) + (defconstant C_KW #c) + + ;; Given the tree hash `environment-hash` of an environment tree E + ;; and the tree hash `parameter-hash` of a constant parameter P + ;; return the tree hash of the tree corresponding to + ;; `(c (q . P) E)` + ;; This is the new environment tree with the addition parameter P curried in. + ;; + ;; Note that `(c (q . P) E)` = `(c . ((q . P) . (E . 0)))` + + (defun-inline update-hash-for-parameter-hash (parameter-hash environment-hash) + (sha256 TWO (sha256 ONE C_KW) + (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) parameter-hash) + (sha256 TWO environment-hash (sha256 ONE 0)))) + ) + + ;; This function recursively calls `update-hash-for-parameter-hash`, updating `environment-hash` + ;; along the way. + + (defun build-curry-list (reversed-curry-parameter-hashes environment-hash) + (if reversed-curry-parameter-hashes + (build-curry-list (r reversed-curry-parameter-hashes) + (update-hash-for-parameter-hash (f reversed-curry-parameter-hashes) environment-hash)) + environment-hash + ) + ) + + ;; Given the tree hash `environment-hash` of an environment tree E + ;; and the tree hash `function-hash` of a function tree F + ;; return the tree hash of the tree corresponding to + ;; `(a (q . F) E)` + ;; This is the hash of a new function that adopts the new environment E. + ;; This is used to build of the tree hash of a curried function. + ;; + ;; Note that `(a (q . F) E)` = `(a . ((q . F) . (E . 0)))` + + (defun-inline tree-hash-of-apply (function-hash environment-hash) + (sha256 TWO (sha256 ONE A_KW) + (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) function-hash) + (sha256 TWO environment-hash (sha256 ONE 0)))) + ) + + ;; function-hash: + ;; the hash of a puzzle function, ie. a `mod` + ;; + ;; reversed-curry-parameter-hashes: + ;; a list of pre-hashed trees representing parameters to be curried into the puzzle. + ;; Note that this must be applied in REVERSED order. This may seem strange, but it greatly simplifies + ;; the underlying code, since we calculate the tree hash from the bottom nodes up, and the last + ;; parameters curried must have their hashes calculated first. + ;; + ;; we return the hash of the curried expression + ;; (a (q . function-hash) (c (cp1 (c cp2 (c ... 1)...)))) + + (defun puzzle-hash-of-curried-function (function-hash . reversed-curry-parameter-hashes) + (tree-hash-of-apply function-hash + (build-curry-list reversed-curry-parameter-hashes (sha256 ONE ONE))) + ) +) diff --git a/chia/wallet/puzzles/did_innerpuz.clvm b/chia/wallet/puzzles/did_innerpuz.clvm index 992ac647823a..c4097969e5cf 100644 --- a/chia/wallet/puzzles/did_innerpuz.clvm +++ b/chia/wallet/puzzles/did_innerpuz.clvm @@ -1,23 +1,25 @@ -(mod (MOD_HASH MY_PUBKEY RECOVERY_DID_LIST_HASH NUM_VERIFICATIONS_REQUIRED mode amount message my_id my_puzhash parent_innerpuzhash_amounts_for_recovery_ids pubkey recovery_list_reveal) +(mod + ( + MY_PUBKEY + RECOVERY_DID_LIST_HASH + NUM_VERIFICATIONS_REQUIRED + Truths + mode + amount + message + new_inner_puzhash + parent_innerpuzhash_amounts_for_recovery_ids + pubkey + recovery_list_reveal + ) ;message is the new puzzle in the recovery and standard spend cases ;MOD_HASH, MY_PUBKEY, RECOVERY_DID_LIST_HASH are curried into the puzzle ;EXAMPLE SOLUTION (0xcafef00d 0x12341234 0x923bf9a7856b19d335a65f12d68957d497e1f0c16c0e14baf6d120e60753a1ce 2 1 100 (q "source code") 0xdeadbeef 0xcafef00d ((0xdadadada 0xdad5dad5 200) () (0xfafafafa 0xfaf5faf5 200)) 0xfadeddab (0x22222222 0x33333333 0x44444444)) (include condition_codes.clvm) - - (defmacro and ARGS - (if ARGS - (qq (if (unquote (f ARGS)) - (unquote (c and (r ARGS))) - () - )) - 1) - ) - - (defmacro not (ARGS) - (qq (if (unquote ARGS) 0 1)) - ) + (include curry-and-treehash.clinc) + (include singleton_truths.clib) (defun is-in-list (atom items) ;; returns 1 iff `atom` is in the list of `items` @@ -38,41 +40,6 @@ ) ) - ;; utility function used by `curry_args` - (defun fix_curry_args (items core) - (if items - (qq (c (q . (unquote (f items))) (unquote (fix_curry_args (r items) core)))) - core - ) - ) - - ; (curry_args sum (list 50 60)) => returns a function that is like (sum 50 60 ...) - (defun curry_args (func list_of_args) (qq (a (q . (unquote func)) (unquote (fix_curry_args list_of_args (q . 1)))))) - - ;; (curry sum 50 60) => returns a function that is like (sum 50 60 ...) - (defun curry (func . args) (curry_args func args)) - - ;; hash a tree with escape values representing already-hashed subtrees - ;; This optimization can be useful if you know the puzzle hash of a sub-expression. - ;; You probably actually want to use `curry_and_hash` though. - (defun sha256tree_esc_list - (TREE LITERALS) - (if (l TREE) - (sha256 2 (sha256tree_esc_list (f TREE) LITERALS) (sha256tree_esc_list (r TREE) LITERALS)) - (if (is-in-list TREE LITERALS) - TREE - (sha256 1 TREE) - ) - ) - ) - - ;; hash a tree with escape values representing already-hashed subtrees - ;; This optimization can be useful if you know the tree hash of a sub-expression. - (defun sha256tree_esc - (TREE . LITERAL) - (sha256tree_esc_list TREE LITERAL) - ) - ;recovery message module - gets values curried in to make the puzzle ;TODO - this should probably be imported (defun make_message_puzzle (recovering_coin newpuz pubkey) @@ -83,32 +50,26 @@ (list ASSERT_COIN_ANNOUNCEMENT (sha256 (sha256 coin_id (sha256tree1 (make_message_puzzle my_id new_innerpuz pubkey))) my_id)) ) - ;; return the puzzle hash for a cc with the given `genesis-coin-checker-hash` & `inner-puzzle` - (defun-inline create_fullpuzhash (mod_hash mod_hash_hash genesis_id inner_puzzle_hash) - (sha256tree_esc (curry mod_hash mod_hash_hash genesis_id inner_puzzle_hash) - mod_hash - mod_hash_hash - inner_puzzle_hash) +(defun-inline create_coin_ID_for_recovery (MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH parent innerpuzhash amount) + (sha256 parent (calculate_full_puzzle_hash MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH innerpuzhash) amount) ) - (defun-inline create_coin_ID_for_recovery (mod_hash mod_hash_hash did parent innerpuzhash amount) - (sha256 parent (create_fullpuzhash mod_hash mod_hash_hash did innerpuzhash) amount) - ) - - (defmacro recreate_self (my_puzhash amount) - (qq (c CREATE_COIN (c (unquote my_puzhash) (c (unquote amount) ())))) + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH inner_puzzle_hash) + (puzzle-hash-of-curried-function MOD_HASH inner_puzzle_hash (sha256 1 LAUNCHER_PUZZLE_HASH) (sha256 1 LAUNCHER_ID) (sha256 1 MOD_HASH)) ) (defmacro create_new_coin (amount new_puz) (qq (c CREATE_COIN (c (unquote new_puz) (c (unquote amount) ())))) ) - (defun check_messages_from_identities (mod_hash mod_hash_hash num_verifications_required identities my_id output new_puz parent_innerpuzhash_amounts_for_recovery_ids pubkey num_verifications) + (defun check_messages_from_identities (MOD_HASH LAUNCHER_PUZZLE_HASH num_verifications_required identities my_id output new_puz parent_innerpuzhash_amounts_for_recovery_ids pubkey num_verifications) (if identities (if (f parent_innerpuzhash_amounts_for_recovery_ids) (check_messages_from_identities - mod_hash - mod_hash_hash + MOD_HASH + LAUNCHER_PUZZLE_HASH num_verifications_required (r identities) my_id @@ -116,9 +77,9 @@ (create_consume_message ; create coin_id from DID (create_coin_ID_for_recovery - mod_hash - mod_hash_hash + MOD_HASH (f identities) + LAUNCHER_PUZZLE_HASH (f (f parent_innerpuzhash_amounts_for_recovery_ids)) (f (r (f parent_innerpuzhash_amounts_for_recovery_ids))) (f (r (r (f parent_innerpuzhash_amounts_for_recovery_ids))))) @@ -132,9 +93,8 @@ (+ num_verifications 1) ) (check_messages_from_identities - mod_hash - mod_hash_hash - num_verifications_required + MOD_HASH + LAUNCHER_PUZZLE_HASH (r identities) my_id output @@ -155,29 +115,36 @@ ) ) + (defun create_messages (messages) + (if messages + (c (list CREATE_COIN (f messages) 0) (create_messages (r messages))) + () + ) + ) + ;Spend modes: - ;0 = normal spend - ;1 = attest - ;2 (or anything else) = recovery + ;0 = exit spend + ;1 = create messages and recreate singleton + ;2 = recovery ;MAIN (if mode (if (= mode 1) - ; mode one - create message - (list (recreate_self my_puzhash amount) (list CREATE_COIN message 0) (list AGG_SIG_ME MY_PUBKEY (sha256tree1 (list my_puzhash amount message)))) + ; mode one - create messages and recreate singleton + (c (list CREATE_COIN new_inner_puzhash amount) (c (list AGG_SIG_ME MY_PUBKEY (sha256tree1 (list new_inner_puzhash amount message))) (create_messages message))) ; mode two - recovery ; check that recovery list is not empty (if recovery_list_reveal (if (= (sha256tree1 recovery_list_reveal) RECOVERY_DID_LIST_HASH) - (check_messages_from_identities MOD_HASH (sha256tree1 MOD_HASH) NUM_VERIFICATIONS_REQUIRED recovery_list_reveal my_id (list (create_new_coin amount message)) message parent_innerpuzhash_amounts_for_recovery_ids pubkey 0) + (check_messages_from_identities (singleton_mod_hash_truth Truths) (singleton_launcher_puzzle_hash_truth Truths) NUM_VERIFICATIONS_REQUIRED recovery_list_reveal (my_id_truth Truths) (list (create_new_coin amount message)) message parent_innerpuzhash_amounts_for_recovery_ids pubkey 0) (x) ) (x) ) ) - ; mode zero - normal spend - (list (create_new_coin amount message) (list AGG_SIG_ME MY_PUBKEY (sha256tree1 (list amount message)))) + ; mode zero - exit spend + (list (list CREATE_COIN 0x00 -113) (list CREATE_COIN message amount) (list AGG_SIG_ME MY_PUBKEY (sha256tree1 (list amount message)))) ) ) diff --git a/chia/wallet/puzzles/did_innerpuz.clvm.hex b/chia/wallet/puzzles/did_innerpuz.clvm.hex index 839c62a3e6cc..968888f29277 100644 --- a/chia/wallet/puzzles/did_innerpuz.clvm.hex +++ b/chia/wallet/puzzles/did_innerpuz.clvm.hex @@ -1 +1 @@ -ff02ffff01ff02ffff03ff5fffff01ff02ffff03ffff09ff5fffff010180ffff01ff04ffff04ff24ffff04ff8205ffffff04ff81bfff80808080ffff04ffff04ff24ffff04ff82017fffff01ff80808080ffff04ffff04ff10ffff04ff0bffff04ffff02ff36ffff04ff02ffff04ffff04ff8205ffffff04ff81bfffff04ff82017fff80808080ff80808080ff80808080ff80808080ffff01ff02ffff03ff822fffffff01ff02ffff03ffff09ffff02ff36ffff04ff02ffff04ff822fffff80808080ff1780ffff01ff02ff2cffff04ff02ffff04ff05ffff04ffff02ff36ffff04ff02ffff04ff05ff80808080ffff04ff2fffff04ff822fffffff04ff8202ffffff04ffff04ffff04ff24ffff04ff82017fffff04ff81bfff80808080ff8080ffff04ff82017fffff04ff820bffffff04ff8217ffffff01ff80808080808080808080808080ffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff01ff04ffff04ff24ffff04ff82017fffff04ff81bfff80808080ffff04ffff04ff10ffff04ff0bffff04ffff02ff36ffff04ff02ffff04ffff04ff81bfffff04ff82017fff808080ff80808080ff80808080ff80808080ff0180ffff04ffff01ffffff32ff313dffff333cffff02ffff03ff2fffff01ff02ffff03ff8204ffffff01ff02ff2cffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff6fffff04ff5fffff04ffff04ffff04ff38ffff04ffff0bffff0bffff0bff8208ffffff02ff2effff04ff02ffff04ffff02ff3cffff04ff02ffff04ff05ffff04ff0bffff04ff4fffff04ff8214ffff80808080808080ffff04ff05ffff04ff0bffff04ff8214ffff80808080808080ff822cff80ffff02ff36ffff04ff02ffff04ffff02ff26ffff04ff02ffff04ff5fffff04ff82017fffff04ff8205ffff808080808080ff8080808080ff5f80ff808080ff81bf80ffff04ff82017fffff04ff8206ffffff04ff8205ffffff04ffff10ff820bffffff010180ff80808080808080808080808080ffff01ff02ff2cffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff6fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8206ffffff04ff8205ffffff04ff820bffff8080808080808080808080808080ff0180ffff01ff02ffff03ffff15ff820bffff1780ffff01ff04ffff04ff28ffff04ff8205ffffff04ff82017fff80808080ff81bf80ffff01ff02ffff03ffff09ff820bffff1780ffff01ff04ffff04ff28ffff04ff8205ffffff04ff82017fff80808080ff81bf80ffff01ff08ffff01986e6f7420656e6f75676820766572696669636174696f6e738080ff018080ff018080ff0180ff02ff12ffff04ff02ffff04ff05ffff04ff07ff8080808080ffffff04ffff0102ffff04ffff04ffff0101ff0580ffff04ffff02ff2affff04ff02ffff04ff0bffff01ff0180808080ff80808080ffff02ffff03ff05ffff01ff04ffff0104ffff04ffff04ffff0101ff0980ffff04ffff02ff2affff04ff02ffff04ff0dffff04ff0bff8080808080ff80808080ffff010b80ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff05ff1380ffff01ff0101ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ff8080ff0180ffffff04ffff0101ffff04ffff04ff34ffff04ff05ff808080ffff04ffff04ff28ffff04ff17ffff04ff0bff80808080ff80808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ff3effff04ff02ffff04ff05ffff04ff07ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ffff04ff0bff8080808080ffff02ff3effff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff02ffff03ffff02ff3affff04ff02ffff04ff05ffff04ff0bff8080808080ffff0105ffff01ff0bffff0101ff058080ff018080ff0180ff018080 \ No newline at end of file +ff02ffff01ff02ffff03ff5fffff01ff02ffff03ffff09ff5fffff010180ffff01ff04ffff04ff24ffff04ff8202ffffff04ff81bfff80808080ffff04ffff04ff20ffff04ff05ffff04ffff02ff3effff04ff02ffff04ffff04ff8202ffffff04ff81bfffff04ff82017fff80808080ff80808080ff80808080ffff02ff26ffff04ff02ffff04ff82017fff808080808080ffff01ff02ffff03ff8217ffffff01ff02ffff03ffff09ffff02ff3effff04ff02ffff04ff8217ffff80808080ff0b80ffff01ff02ff3affff04ff02ffff04ff8202efffff04ff820befffff04ff17ffff04ff8217ffffff04ff818fffff04ffff04ffff04ff24ffff04ff82017fffff04ff81bfff80808080ff8080ffff04ff82017fffff04ff8205ffffff04ff820bffffff01ff80808080808080808080808080ffff01ff088080ff0180ffff01ff088080ff018080ff0180ffff01ff04ffff04ff24ffff01ff00ff818f8080ffff04ffff04ff24ffff04ff82017fffff04ff81bfff80808080ffff04ffff04ff20ffff04ff05ffff04ffff02ff3effff04ff02ffff04ffff04ff81bfffff04ff82017fff808080ff80808080ff80808080ff8080808080ff0180ffff04ffff01ffffffff3231ff3d02ffff333cff0401ffffff0102ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff32ffff0bff3cff2c80ffff0bff32ffff0bff32ffff0bff3cff2280ff0980ffff0bff32ff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff2fffff01ff02ffff03ff8204ffffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff6fffff04ff5fffff04ffff04ffff04ff28ffff04ffff0bffff0bffff0bff8208ffffff02ff2effff04ff02ffff04ff05ffff04ff8214ffffff04ffff0bffff0101ff0b80ffff04ffff0bffff0101ff4f80ffff04ffff0bffff0101ff0580ff8080808080808080ff822cff80ffff02ff3effff04ff02ffff04ffff02ff36ffff04ff02ffff04ff5fffff04ff82017fffff04ff8205ffff808080808080ff8080808080ff5f80ff808080ff81bf80ffff04ff82017fffff04ff8206ffffff04ff8205ffffff04ffff10ff820bffffff010180ff80808080808080808080808080ffff01ff02ff3affff04ff02ffff04ff05ffff04ff0bffff04ff6fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8206ffffff04ff8205ffffff04ff820bffff80808080808080808080808080ff0180ffff01ff02ffff03ffff15ff820bffff1780ffff01ff04ffff04ff30ffff04ff8205ffffff04ff82017fff80808080ff81bf80ffff01ff02ffff03ffff09ff820bffff1780ffff01ff04ffff04ff30ffff04ff8205ffffff04ff82017fff80808080ff81bf80ffff01ff08ffff01986e6f7420656e6f75676820766572696669636174696f6e738080ff018080ff018080ff0180ffffff02ffff03ff05ffff01ff04ffff04ff24ffff04ff09ffff01ff80808080ffff02ff26ffff04ff02ffff04ff0dff8080808080ff8080ff0180ff04ffff0101ffff04ffff04ff34ffff04ff05ff808080ffff04ffff04ff30ffff04ff17ffff04ff0bff80808080ff80808080ffff0bff32ffff0bff3cff3880ffff0bff32ffff0bff32ffff0bff3cff2280ff0580ffff0bff32ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree b/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree index a835a389f8c9..87025ac8c317 100644 --- a/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree +++ b/chia/wallet/puzzles/did_innerpuz.clvm.hex.sha256tree @@ -1 +1 @@ -2f6e9a0237d200ac3b989d7c825ab68d732db6a2d1c8018e9f79d4be329eeed0 +f2356bc00a27abf46c72b809ba7d1d53bde533d94a7a3da8954155afe54304c4 diff --git a/chia/wallet/puzzles/p2_singleton.clvm b/chia/wallet/puzzles/p2_singleton.clvm new file mode 100644 index 000000000000..d3d7e64d7430 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton.clvm @@ -0,0 +1,38 @@ +(mod (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash my_id) + + ; SINGLETON_MOD_HASH is the mod-hash for the singleton_top_layer puzzle + ; LAUNCHER_ID is the ID of the singleton we are commited to paying to + ; LAUNCHER_PUZZLE_HASH is the puzzle hash of the launcher + ; singleton_inner_puzzle_hash is the innerpuzzlehash for our singleton at the current time + ; my_id is the coin_id of the coin that this puzzle is locked into + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + + ; takes a lisp tree and returns the hash of it + (defun sha256tree (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 1 TREE) + ) + ) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH inner_puzzle_hash) + (puzzle-hash-of-curried-function SINGLETON_MOD_HASH + inner_puzzle_hash + (sha256tree (c SINGLETON_MOD_HASH (c LAUNCHER_ID LAUNCHER_PUZZLE_HASH))) + ) + ) + + (defun-inline claim_rewards (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash my_id) + (list + (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash) my_id)) + (list CREATE_COIN_ANNOUNCEMENT '$') + (list ASSERT_MY_COIN_ID my_id)) + ) + + ; main + (claim_rewards SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash my_id) +) diff --git a/chia/wallet/puzzles/p2_singleton.clvm.hex b/chia/wallet/puzzles/p2_singleton.clvm.hex new file mode 100644 index 000000000000..00e1fdcabf83 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff18ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff2fffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0bff178080ff80808080ff808080808080ff5f80ff808080ffff04ffff04ff2cffff01ff248080ffff04ffff04ff10ffff04ff5fff808080ff80808080ffff04ffff01ffffff463fff02ff3c04ffff01ff0102ffff02ffff03ff05ffff01ff02ff16ffff04ff02ffff04ff0dffff04ffff0bff3affff0bff12ff3c80ffff0bff3affff0bff3affff0bff12ff2a80ff0980ffff0bff3aff0bffff0bff12ff8080808080ff8080808080ffff010b80ff0180ffff0bff3affff0bff12ff1480ffff0bff3affff0bff3affff0bff12ff2a80ff0580ffff0bff3affff02ff16ffff04ff02ffff04ff07ffff04ffff0bff12ff1280ff8080808080ffff0bff12ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/p2_singleton.clvm.hex.sha256tree b/chia/wallet/puzzles/p2_singleton.clvm.hex.sha256tree new file mode 100644 index 000000000000..8c26f84e1d4b --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton.clvm.hex.sha256tree @@ -0,0 +1 @@ +40f828d8dd55603f4ff9fbf6b73271e904e69406982f4fbefae2c8dcceaf9834 diff --git a/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm b/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm new file mode 100644 index 000000000000..73ac6b242985 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm @@ -0,0 +1,58 @@ +(mod (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH SECONDS_DELAY DELAYED_PUZZLE_HASH p1 my_id) + + ;; This puzzle has two escape conditions: the regular "claim via singleton", and the + ;; delayed "claim via puzzle hash", delayed by a fixed number of seconds. + + ; SINGLETON_MOD_HASH is the mod-hash for the singleton_top_layer puzzle + ; LAUNCHER_ID is the ID of the singleton we are commited to paying to + ; LAUNCHER_PUZZLE_HASH is the puzzle hash of the launcher + ; SECONDS_DELAY is the number of seconds before the coin can be spent with `DELAYED_PUZZLE_HASH` + ; DELAYED_PUZZLE_HASH is the puzzle hash of the delayed puzzle + ; if my_id is passed in as () then this signals that we are trying to do a delayed spend case + ; p1's meaning changes depending upon which case we're using + ; if we are paying to singleton then p1 is singleton_inner_puzzle_hash + ; if we are running the delayed case then p1 is the amount to output + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + + ; takes a lisp tree and returns the hash of it + (defun sha256tree (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 1 TREE) + ) + ) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline delayed_spend (SECONDS_DELAY DELAYED_PUZZLE_HASH amount) + (list + (list ASSERT_SECONDS_RELATIVE SECONDS_DELAY) + (list CREATE_COIN DELAYED_PUZZLE_HASH amount) + (list ASSERT_MY_AMOUNT amount) + ) + ) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH inner_puzzle_hash) + (puzzle-hash-of-curried-function SINGLETON_MOD_HASH + inner_puzzle_hash + (sha256tree (c SINGLETON_MOD_HASH (c LAUNCHER_ID LAUNCHER_PUZZLE_HASH))) + ) + ) + + (defun-inline claim_rewards (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash my_id) + (list + (list ASSERT_PUZZLE_ANNOUNCEMENT (sha256 (calculate_full_puzzle_hash SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH singleton_inner_puzzle_hash) my_id)) + (list CREATE_COIN_ANNOUNCEMENT '$') + (list ASSERT_MY_COIN_ID my_id)) + ) + + ; main + (if my_id + (claim_rewards SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PUZZLE_HASH p1 my_id) + (delayed_spend SECONDS_DELAY DELAYED_PUZZLE_HASH p1) + ) +) diff --git a/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex b/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex new file mode 100644 index 000000000000..fa458404a98f --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ff82017fffff01ff04ffff04ff38ffff04ffff0bffff02ff2effff04ff02ffff04ff05ffff04ff81bfffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0bff178080ff80808080ff808080808080ff82017f80ff808080ffff04ffff04ff3cffff01ff248080ffff04ffff04ff28ffff04ff82017fff808080ff80808080ffff01ff04ffff04ff24ffff04ff2fff808080ffff04ffff04ff2cffff04ff5fffff04ff81bfff80808080ffff04ffff04ff10ffff04ff81bfff808080ff8080808080ff0180ffff04ffff01ffffff49ff463fffff5002ff333cffff04ff0101ffff02ff02ffff03ff05ffff01ff02ff36ffff04ff02ffff04ff0dffff04ffff0bff26ffff0bff2aff1280ffff0bff26ffff0bff26ffff0bff2aff3a80ff0980ffff0bff26ff0bffff0bff2aff8080808080ff8080808080ffff010b80ff0180ffff0bff26ffff0bff2aff3480ffff0bff26ffff0bff26ffff0bff2aff3a80ff0580ffff0bff26ffff02ff36ffff04ff02ffff04ff07ffff04ffff0bff2aff2a80ff8080808080ffff0bff2aff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex.sha256tree b/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex.sha256tree new file mode 100644 index 000000000000..44570a777aa9 --- /dev/null +++ b/chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm.hex.sha256tree @@ -0,0 +1 @@ +adb656e0211e2ab4f42069a4c5efc80dc907e7062be08bf1628c8e5b6d94d25b diff --git a/chia/wallet/puzzles/pool_member_innerpuz.clvm b/chia/wallet/puzzles/pool_member_innerpuz.clvm new file mode 100644 index 000000000000..182d62ff7ac8 --- /dev/null +++ b/chia/wallet/puzzles/pool_member_innerpuz.clvm @@ -0,0 +1,70 @@ +(mod (POOL_PUZZLE_HASH + P2_SINGLETON_PUZZLE_HASH + OWNER_PUBKEY + POOL_REWARD_PREFIX + WAITINGROOM_PUZHASH + Truths + p1 + pool_reward_height + ) + + + ; POOL_PUZZLE_HASH is commitment to the pool's puzzle hash + ; P2_SINGLETON_PUZZLE_HASH is the puzzle hash for your pay to singleton puzzle + ; OWNER_PUBKEY is the farmer pubkey which authorises a travel + ; POOL_REWARD_PREFIX is network-specific data (mainnet vs testnet) that helps determine if a coin is a pool reward + ; WAITINGROOM_PUZHASH is the puzzle_hash you'll go to when you iniate the leaving process + + ; Absorbing money if pool_reward_height is an atom + ; Escaping if pool_reward_height is () + + ; p1 is pool_reward_amount if absorbing money + ; p1 is key_value_list if escaping + + ; pool_reward_amount is the value of the coin reward - this is passed in so that this puzzle will still work after halvenings + ; pool_reward_height is the block height that the reward was generated at. This is used to calculate the coin ID. + ; key_value_list is signed extra data that the wallet may want to publicly announce for syncing purposes + + (include condition_codes.clvm) + (include singleton_truths.clib) + + ; takes a lisp tree and returns the hash of it + (defun sha256tree (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 1 TREE) + ) + ) + + (defun-inline calculate_pool_reward (pool_reward_height P2_SINGLETON_PUZZLE_HASH POOL_REWARD_PREFIX pool_reward_amount) + (sha256 (logior POOL_REWARD_PREFIX (logand (- (lsh (q . 1) (q . 128)) (q . 1)) pool_reward_height)) P2_SINGLETON_PUZZLE_HASH pool_reward_amount) + ) + + (defun absorb_pool_reward (POOL_PUZZLE_HASH my_inner_puzzle_hash my_amount pool_reward_amount pool_reward_id) + (list + (list CREATE_COIN my_inner_puzzle_hash my_amount) + (list CREATE_COIN POOL_PUZZLE_HASH pool_reward_amount) + (list CREATE_PUZZLE_ANNOUNCEMENT pool_reward_id) + (list ASSERT_COIN_ANNOUNCEMENT (sha256 pool_reward_id '$')) + ) + ) + + (defun-inline travel_to_waitingroom (OWNER_PUBKEY WAITINGROOM_PUZHASH my_amount extra_data) + (list (list AGG_SIG_ME OWNER_PUBKEY (sha256tree extra_data)) + (list CREATE_COIN WAITINGROOM_PUZHASH my_amount) + ) + ) + + ; main + + (if pool_reward_height + (absorb_pool_reward POOL_PUZZLE_HASH + (my_inner_puzzle_hash_truth Truths) + (my_amount_truth Truths) + p1 + (calculate_pool_reward pool_reward_height P2_SINGLETON_PUZZLE_HASH POOL_REWARD_PREFIX p1) + ) + (travel_to_waitingroom OWNER_PUBKEY WAITINGROOM_PUZHASH (my_amount_truth Truths) p1) + ) + ) +) diff --git a/chia/wallet/puzzles/pool_member_innerpuz.clvm.hex b/chia/wallet/puzzles/pool_member_innerpuz.clvm.hex new file mode 100644 index 000000000000..ba609d292e75 --- /dev/null +++ b/chia/wallet/puzzles/pool_member_innerpuz.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ff8202ffffff01ff02ff16ffff04ff02ffff04ff05ffff04ff8204bfffff04ff8206bfffff04ff82017fffff04ffff0bffff19ff2fffff18ffff019100ffffffffffffffffffffffffffffffffff8202ff8080ff0bff82017f80ff8080808080808080ffff01ff04ffff04ff08ffff04ff17ffff04ffff02ff1effff04ff02ffff04ff82017fff80808080ff80808080ffff04ffff04ff1cffff04ff5fffff04ff8206bfff80808080ff80808080ff0180ffff04ffff01ffff32ff3d33ff3effff04ffff04ff1cffff04ff0bffff04ff17ff80808080ffff04ffff04ff1cffff04ff05ffff04ff2fff80808080ffff04ffff04ff0affff04ff5fff808080ffff04ffff04ff14ffff04ffff0bff5fffff012480ff808080ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/pool_member_innerpuz.clvm.hex.sha256tree b/chia/wallet/puzzles/pool_member_innerpuz.clvm.hex.sha256tree new file mode 100644 index 000000000000..33081b4b3781 --- /dev/null +++ b/chia/wallet/puzzles/pool_member_innerpuz.clvm.hex.sha256tree @@ -0,0 +1 @@ +a8490702e333ddd831a3ac9c22d0fa26d2bfeaf2d33608deb22f0e0123eb0494 diff --git a/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm b/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm new file mode 100644 index 000000000000..545c1f3e093e --- /dev/null +++ b/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm @@ -0,0 +1,69 @@ +(mod (POOL_PUZZLE_HASH + P2_SINGLETON_PUZZLE_HASH + OWNER_PUBKEY + POOL_REWARD_PREFIX + RELATIVE_LOCK_HEIGHT + Truths + spend_type + p1 + p2 + ) + + ; POOL_PUZZLE_HASH is commitment to the pool's puzzle hash + ; P2_SINGLETON_PUZZLE_HASH is the puzzlehash for your pay_to_singleton puzzle + ; OWNER_PUBKEY is the farmer pubkey which signs the exit puzzle_hash + ; POOL_REWARD_PREFIX is network-specific data (mainnet vs testnet) that helps determine if a coin is a pool reward + ; RELATIVE_LOCK_HEIGHT is how long it takes to leave + + ; spend_type is: 0 for absorbing money, 1 to escape + ; if spend_type is 0 + ; p1 is pool_reward_amount - the value of the coin reward - this is passed in so that this puzzle will still work after halvenings + ; p2 is pool_reward_height - the block height that the reward was generated at. This is used to calculate the coin ID. + ; if spend_type is 1 + ; p1 is key_value_list - signed extra data that the wallet may want to publicly announce for syncing purposes + ; p2 is destination_puzhash - the location that the escape spend wants to create itself to + + (include condition_codes.clvm) + (include singleton_truths.clib) + + ; takes a lisp tree and returns the hash of it + (defun sha256tree (TREE) + (if (l TREE) + (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) + (sha256 1 TREE) + ) + ) + + (defun-inline calculate_pool_reward (pool_reward_height P2_SINGLETON_PUZZLE_HASH POOL_REWARD_PREFIX pool_reward_amount) + (sha256 (logior POOL_REWARD_PREFIX (logand (- (lsh (q . 1) (q . 128)) (q . 1)) pool_reward_height)) P2_SINGLETON_PUZZLE_HASH pool_reward_amount) + ) + + (defun absorb_pool_reward (POOL_PUZZLE_HASH my_inner_puzzle_hash my_amount pool_reward_amount pool_reward_id) + (list + (list CREATE_COIN my_inner_puzzle_hash my_amount) + (list CREATE_COIN POOL_PUZZLE_HASH pool_reward_amount) + (list CREATE_PUZZLE_ANNOUNCEMENT pool_reward_id) + (list ASSERT_COIN_ANNOUNCEMENT (sha256 pool_reward_id '$')) + ) + ) + + (defun-inline travel_spend (RELATIVE_LOCK_HEIGHT new_puzzle_hash my_amount extra_data) + (list (list ASSERT_HEIGHT_RELATIVE RELATIVE_LOCK_HEIGHT) + (list CREATE_COIN new_puzzle_hash my_amount) + (list AGG_SIG_ME OWNER_PUBKEY (sha256tree (list new_puzzle_hash extra_data))) + ) + ) + + ; main + + (if spend_type + (travel_spend RELATIVE_LOCK_HEIGHT p2 (my_amount_truth Truths) p1) + (absorb_pool_reward POOL_PUZZLE_HASH + (my_inner_puzzle_hash_truth Truths) + (my_amount_truth Truths) + p1 + (calculate_pool_reward p2 P2_SINGLETON_PUZZLE_HASH POOL_REWARD_PREFIX p1) + ) + ) + +) diff --git a/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex b/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex new file mode 100644 index 000000000000..fab3d61de484 --- /dev/null +++ b/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ff82017fffff01ff04ffff04ff1cffff04ff5fff808080ffff04ffff04ff12ffff04ff8205ffffff04ff8206bfff80808080ffff04ffff04ff08ffff04ff17ffff04ffff02ff1effff04ff02ffff04ffff04ff8205ffffff04ff8202ffff808080ff80808080ff80808080ff80808080ffff01ff02ff16ffff04ff02ffff04ff05ffff04ff8204bfffff04ff8206bfffff04ff8202ffffff04ffff0bffff19ff2fffff18ffff019100ffffffffffffffffffffffffffffffffff8205ff8080ff0bff8202ff80ff808080808080808080ff0180ffff04ffff01ffff32ff3d52ffff333effff04ffff04ff12ffff04ff0bffff04ff17ff80808080ffff04ffff04ff12ffff04ff05ffff04ff2fff80808080ffff04ffff04ff1affff04ff5fff808080ffff04ffff04ff14ffff04ffff0bff5fffff012480ff808080ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff1effff04ff02ffff04ff09ff80808080ffff02ff1effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex.sha256tree b/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex.sha256tree new file mode 100644 index 000000000000..562813256284 --- /dev/null +++ b/chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm.hex.sha256tree @@ -0,0 +1 @@ +a317541a765bf8375e1c6e7c13503d0d2cbf56cacad5182befe947e78e2c0307 diff --git a/chia/wallet/puzzles/prefarm/spend_prefarm.py b/chia/wallet/puzzles/prefarm/spend_prefarm.py index 62cb94b387d8..1b645f5839bc 100644 --- a/chia/wallet/puzzles/prefarm/spend_prefarm.py +++ b/chia/wallet/puzzles/prefarm/spend_prefarm.py @@ -7,13 +7,29 @@ from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.types.blockchain_format.program import Program from chia.types.coin_solution import CoinSolution +from chia.types.condition_opcodes import ConditionOpcode from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import decode_puzzle_hash +from chia.util.condition_tools import parse_sexp_to_conditions from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.ints import uint32, uint16 +def print_conditions(spend_bundle: SpendBundle): + print("\nConditions:") + for coin_solution in spend_bundle.coin_solutions: + result = Program.from_bytes(bytes(coin_solution.puzzle_reveal)).run( + Program.from_bytes(bytes(coin_solution.solution)) + ) + error, result_human = parse_sexp_to_conditions(result) + assert error is None + assert result_human is not None + for cvp in result_human: + print(f"{ConditionOpcode(cvp.opcode).name}: {[var.hex() for var in cvp.vars]}") + print("") + + async def main() -> None: rpc_port: uint16 = uint16(8555) self_hostname = "localhost" @@ -42,16 +58,27 @@ async def main() -> None: binutils.assemble(f"(q . ((51 0x{ph1.hex()} {pool_amounts}) (51 0x{ph2.hex()} {pool_amounts})))") ) + print(f"Ph1: {ph1.hex()}") + print(f"Ph2: {ph2.hex()}") + assert ph1.hex() == "1b7ab2079fa635554ad9bd4812c622e46ee3b1875a7813afba127bb0cc9794f9" + assert ph2.hex() == "6f184a7074c925ef8688ce56941eb8929be320265f824ec7e351356cc745d38a" + p_solution = Program.to(binutils.assemble("()")) sb_farmer = SpendBundle([CoinSolution(farmer_prefarm, p_farmer_2, p_solution)], G2Element()) sb_pool = SpendBundle([CoinSolution(pool_prefarm, p_pool_2, p_solution)], G2Element()) - print(sb_pool, sb_farmer) - res = await client.push_tx(sb_farmer) + print("\n\n\nConditions") + print_conditions(sb_pool) + print("\n\n\n") + print("Farmer to spend") + print(sb_pool) + print(sb_farmer) + print("\n\n\n") + # res = await client.push_tx(sb_farmer) # res = await client.push_tx(sb_pool) - print(res) + # print(res) up = await client.get_coin_records_by_puzzle_hash(farmer_prefarm.puzzle_hash, True) uf = await client.get_coin_records_by_puzzle_hash(pool_prefarm.puzzle_hash, True) print(up) diff --git a/chia/wallet/puzzles/singleton_launcher.clvm b/chia/wallet/puzzles/singleton_launcher.clvm new file mode 100644 index 000000000000..2636730f1afa --- /dev/null +++ b/chia/wallet/puzzles/singleton_launcher.clvm @@ -0,0 +1,16 @@ +(mod (singleton_full_puzzle_hash amount key_value_list) + + (include condition_codes.clvm) + + ; takes a lisp tree and returns the hash of it + (defun sha256tree1 (TREE) + (if (l TREE) + (sha256 2 (sha256tree1 (f TREE)) (sha256tree1 (r TREE))) + (sha256 1 TREE) + ) + ) + + ; main + (list (list CREATE_COIN singleton_full_puzzle_hash amount) + (list CREATE_COIN_ANNOUNCEMENT (sha256tree1 (list singleton_full_puzzle_hash amount key_value_list)))) +) diff --git a/chia/wallet/puzzles/singleton_launcher.clvm.hex b/chia/wallet/puzzles/singleton_launcher.clvm.hex new file mode 100644 index 000000000000..2e4e1f69a2b7 --- /dev/null +++ b/chia/wallet/puzzles/singleton_launcher.clvm.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff04ffff04ff05ffff04ff0bff80808080ffff04ffff04ff0affff04ffff02ff0effff04ff02ffff04ffff04ff05ffff04ff0bffff04ff17ff80808080ff80808080ff808080ff808080ffff04ffff01ff33ff3cff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff0effff04ff02ffff04ff09ff80808080ffff02ff0effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/singleton_launcher.clvm.hex.sha256tree b/chia/wallet/puzzles/singleton_launcher.clvm.hex.sha256tree new file mode 100644 index 000000000000..fb2621f763ee --- /dev/null +++ b/chia/wallet/puzzles/singleton_launcher.clvm.hex.sha256tree @@ -0,0 +1 @@ +eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9 diff --git a/chia/wallet/puzzles/singleton_top_layer.clvm b/chia/wallet/puzzles/singleton_top_layer.clvm index bfcd62b38b19..db762a4c9a1b 100644 --- a/chia/wallet/puzzles/singleton_top_layer.clvm +++ b/chia/wallet/puzzles/singleton_top_layer.clvm @@ -1,23 +1,18 @@ -(mod (mod_hash genesis_puzhash innerpuz (genesis_parent genesis_amount) parent_info my_amount inner_solution) +(mod (SINGLETON_STRUCT INNER_PUZZLE lineage_proof my_amount inner_solution) -;mod_hash, genesis_puzhash, innerpuz are curried in +;; SINGLETON_STRUCT = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH)) -; EXAMPLE SOLUTION '(0xfadeddab 0xdeadbeef 1 (0xdeadbeef 0xcafef00d 200) 50 ((51 0xfadeddab 100) (60 "trash") (51 deadbeef 0)))' +; SINGLETON_STRUCT, INNER_PUZZLE are curried in by the wallet -(include condition_codes.clvm) +; EXAMPLE SOLUTION '(0xfadeddab 0xdeadbeef 1 (0xdeadbeef 200) 50 ((51 0xfadeddab 100) (60 "trash") (51 deadbeef 0)))' -; This is for the core - (defun is-in-list (atom items) - ;; returns 1 iff `atom` is in the list of `items` - (if items - (if (= atom (f items)) - 1 - (is-in-list atom (r items)) - ) - 0 - ) - ) +; This puzzle is a wrapper around an inner smart puzzle which guarantees uniqueness. +; It takes its singleton identity from a coin with a launcher puzzle which guarantees that it is unique. + + (include condition_codes.clvm) + (include curry-and-treehash.clinc) + (include singleton_truths.clib) ; takes a lisp tree and returns the hash of it (defun sha256tree1 (TREE) @@ -27,89 +22,156 @@ ) ) - ;; utility function used by `curry_args` - (defun fix_curry_args (items core) - (if items - (qq (c (q . (unquote (f items))) (unquote (fix_curry_args (r items) core)))) - core - ) - ) + ; "assert" is a macro that wraps repeated instances of "if" + ; usage: (assert A0 A1 ... An R) + ; all of A0, A1, ... An must evaluate to non-null, or an exception is raised + ; return the value of R (if we get that far) - ; (curry_args sum (list 50 60)) => returns a function that is like (sum 50 60 ...) - (defun curry_args (func list_of_args) (qq (a (q . (unquote func)) (unquote (fix_curry_args list_of_args (q . 1)))))) - - ;; (curry sum 50 60) => returns a function that is like (sum 50 60 ...) - (defun curry (func . args) (curry_args func args)) - - ;; hash a tree with escape values representing already-hashed subtrees - ;; This optimization can be useful if you know the puzzle hash of a sub-expression. - ;; You probably actually want to use `curry_and_hash` though. - (defun sha256tree_esc_list - (TREE LITERALS) - (if (l TREE) - (sha256 2 (sha256tree_esc_list (f TREE) LITERALS) (sha256tree_esc_list (r TREE) LITERALS)) - (if (is-in-list TREE LITERALS) - TREE - (sha256 1 TREE) - ) - ) - ) - - ;; hash a tree with escape values representing already-hashed subtrees - ;; This optimization can be useful if you know the tree hash of a sub-expression. - (defun sha256tree_esc - (TREE . LITERAL) - (sha256tree_esc_list TREE LITERAL) - ) - - ;; return the puzzle hash for a cc with the given `genesis-coin-checker-hash` & `inner-puzzle` - (defun-inline create_fullpuzhash (mod_hash mod_hash_hash genesis_id inner_puzzle_hash) - (sha256tree_esc (curry mod_hash mod_hash_hash genesis_id inner_puzzle_hash) - mod_hash - mod_hash_hash - inner_puzzle_hash) - ) - - ; assembles information from the solution to create our own full ID including asserting our parent is a coloured coin - (defun-inline create_my_ID (mod_hash mod_hash_hash genesis_puzhash innerpuzhash parent_parent parent_innerpuz parent_amount my_amount) - (sha256 (sha256 parent_parent (create_fullpuzhash mod_hash mod_hash_hash genesis_puzhash parent_innerpuz) parent_amount) (create_fullpuzhash mod_hash mod_hash_hash genesis_puzhash my_innerpuzhash) my_amount) - ) - - (defun check_my_amount_and_ID (mod_hash mod_hash_hash genesis_puzhash genesis_parent genesis_amount my_innerpuzhash parent_info my_amount) - (if (logand my_amount 1) - (if (l parent_info) - (list ASSERT_MY_COIN_ID (create_my_ID mod_hash mod_hash_hash genesis_puzhash my_innerpuzhash (f parent_info) (f (r parent_info)) (f (r (r parent_info))) my_amount)) - (list ASSERT_MY_COIN_ID (sha256 (sha256 genesis_parent genesis_puzhash genesis_amount) (create_fullpuzhash mod_hash mod_hash_hash genesis_puzhash my_innerpuzhash) my_amount)) + (defmacro assert items + (if (r items) + (list if (f items) (c assert (r items)) (q . (x))) + (f items) ) - (x) + ) + + (defun-inline mod_hash_for_singleton_struct (SINGLETON_STRUCT) (f SINGLETON_STRUCT)) + (defun-inline launcher_id_for_singleton_struct (SINGLETON_STRUCT) (f (r SINGLETON_STRUCT))) + (defun-inline launcher_puzzle_hash_for_singleton_struct (SINGLETON_STRUCT) (r (r SINGLETON_STRUCT))) + + ;; return the full puzzlehash for a singleton with the innerpuzzle curried in + ; puzzle-hash-of-curried-function is imported from curry-and-treehash.clinc + (defun-inline calculate_full_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) + (puzzle-hash-of-curried-function (mod_hash_for_singleton_struct SINGLETON_STRUCT) + inner_puzzle_hash + (sha256tree1 SINGLETON_STRUCT) + ) + ) + + ; assembles information from the solution to create our own full ID including asserting our parent is a singleton + (defun create_my_ID (SINGLETON_STRUCT full_puzzle_hash parent_parent parent_inner_puzzle_hash parent_amount my_amount) + (sha256 (sha256 parent_parent (calculate_full_puzzle_hash SINGLETON_STRUCT parent_inner_puzzle_hash) parent_amount) + full_puzzle_hash + my_amount) + ) + + ;; take a boolean and a non-empty list of conditions + ;; strip off the first condition if a boolean is set + ;; this is used to remove `(CREATE_COIN xxx -113)` + ;; pretty sneaky, eh? + (defun strip_first_condition_if (boolean condition_list) + (if boolean + (r condition_list) + condition_list ) ) - ; Check that only one output with odd value exists - (defun check_outputs_value (outputs_loop flag) - (if outputs_loop - (if (= (f (f outputs_loop)) CREATE_COIN) - (if (logand (f (r (r (f outputs_loop)))) 1) - (if flag - (x) - (check_outputs_value (r outputs_loop) 1) + (defun-inline morph_condition (condition SINGLETON_STRUCT) + (list (f condition) (calculate_full_puzzle_hash SINGLETON_STRUCT (f (r condition))) (f (r (r condition)))) + ) + + ;; return the value of the coin created if this is a `CREATE_COIN` condition, or 0 otherwise + (defun-inline created_coin_value_or_0 (condition) + (if (= (f condition) CREATE_COIN) + (f (r (r condition))) + 0 + ) + ) + + ;; Returns a (bool . bool) + (defun odd_cons_m113 (output_amount) + (c + (= (logand output_amount 1) 1) ;; is it odd? + (= output_amount -113) ;; is it the escape value? + ) + ) + + ; Assert exactly one output with odd value exists - ignore it if value is -113 + + ;; this function iterates over the output conditions from the inner puzzle & solution + ;; and both checks that exactly one unique singleton child is created (with odd valued output), + ;; and wraps the inner puzzle with this same singleton wrapper puzzle + ;; + ;; The special case where the output value is -113 means a child singleton is intentionally + ;; *NOT* being created, thus forever ending this singleton's existence + + (defun check_and_morph_conditions_for_singleton (SINGLETON_STRUCT conditions has_odd_output_been_found) + (if conditions + (morph_next_condition SINGLETON_STRUCT conditions has_odd_output_been_found (odd_cons_m113 (created_coin_value_or_0 (f conditions)))) + (if has_odd_output_been_found + 0 + (x) ;; no odd output found ) - (check_outputs_value (r outputs_loop) flag) ) - (check_outputs_value (r outputs_loop) flag) + ) + + ;; a continuation of `check_and_morph_conditions_for_singleton` with booleans `is_output_odd` and `is_output_m113` + ;; precalculated + (defun morph_next_condition (SINGLETON_STRUCT conditions has_odd_output_been_found (is_output_odd . is_output_m113)) + (assert + (not (all is_output_odd has_odd_output_been_found)) + (strip_first_condition_if + is_output_m113 + (c (if is_output_odd + (morph_condition (f conditions) SINGLETON_STRUCT) + (f conditions) + ) + (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (r conditions) (any is_output_odd has_odd_output_been_found)) + ) + ) ) - 1 + ) + + ; this final stager asserts our ID + ; it also runs the innerpuz with the innersolution with the "truths" added + ; it then passes that output conditions from the innerpuz to the morph conditions function + (defun stager_three (SINGLETON_STRUCT lineage_proof my_id full_puzhash innerpuzhash my_amount INNER_PUZZLE inner_solution) + (c (list ASSERT_MY_COIN_ID my_id) (check_and_morph_conditions_for_singleton SINGLETON_STRUCT (a INNER_PUZZLE (c (truth_data_to_truth_struct my_id full_puzhash innerpuzhash my_amount lineage_proof SINGLETON_STRUCT) inner_solution)) 0)) + ) + + ; this checks whether we are an eve spend or not and calculates our full coin ID appropriately and passes it on to the final stager + ; if we are the eve spend it also adds the additional checks that our parent's puzzle is the standard launcher format and that out parent ID is the same as our singleton ID + + (defun stager_two (SINGLETON_STRUCT lineage_proof full_puzhash innerpuzhash my_amount INNER_PUZZLE inner_solution) + (stager_three + SINGLETON_STRUCT + lineage_proof + (if (is_not_eve_proof lineage_proof) + (create_my_ID + SINGLETON_STRUCT + full_puzhash + (parent_info_for_lineage_proof lineage_proof) + (puzzle_hash_for_lineage_proof lineage_proof) + (amount_for_lineage_proof lineage_proof) + my_amount + ) + (if (= + (launcher_id_for_singleton_struct SINGLETON_STRUCT) + (sha256 (parent_info_for_eve_proof lineage_proof) (launcher_puzzle_hash_for_singleton_struct SINGLETON_STRUCT) (amount_for_eve_proof lineage_proof)) + ) + (sha256 (launcher_id_for_singleton_struct SINGLETON_STRUCT) full_puzhash my_amount) + (x) + ) + ) + full_puzhash + innerpuzhash + my_amount + INNER_PUZZLE + inner_solution ) ) - (defun check_id_and_check_outputs (mod_hash mod_hash_hash genesis_puzhash genesis_parent genesis_amount parent_info innerpuzhash my_amount outputs) - (if (check_outputs_value outputs 0) - (c (check_my_amount_and_ID mod_hash mod_hash_hash genesis_puzhash genesis_parent genesis_amount innerpuzhash parent_info my_amount) outputs) - (x) - ) + ; this calculates our current full puzzle hash and passes it to stager two + (defun stager_one (SINGLETON_STRUCT lineage_proof my_innerpuzhash my_amount INNER_PUZZLE inner_solution) + (stager_two SINGLETON_STRUCT lineage_proof (calculate_full_puzzle_hash SINGLETON_STRUCT my_innerpuzhash) my_innerpuzhash my_amount INNER_PUZZLE inner_solution) ) - ;main - (check_id_and_check_outputs mod_hash (sha256 1 mod_hash) genesis_puzhash genesis_parent genesis_amount parent_info (sha256tree1 innerpuz) my_amount (a innerpuz inner_solution)) + ; main + + ; if our value is not an odd amount then we are invalid + ; this calculates my_innerpuzhash and passes all values to stager_one + (if (logand my_amount 1) + (stager_one SINGLETON_STRUCT lineage_proof (sha256tree1 INNER_PUZZLE) my_amount INNER_PUZZLE inner_solution) + (x) + ) ) diff --git a/chia/wallet/puzzles/singleton_top_layer.clvm.hex b/chia/wallet/puzzles/singleton_top_layer.clvm.hex index 4b87a553c939..a1f74bd3a9e3 100644 --- a/chia/wallet/puzzles/singleton_top_layer.clvm.hex +++ b/chia/wallet/puzzles/singleton_top_layer.clvm.hex @@ -1 +1 @@ -ff02ffff01ff02ff38ffff04ff02ffff04ff05ffff04ffff0bffff0101ff0580ffff04ff0bffff04ff4fffff04ff81afffff04ff5fffff04ffff02ff16ffff04ff02ffff04ff17ff80808080ffff04ff81bfffff04ffff02ff17ff82017f80ff808080808080808080808080ffff04ffff01ffffff46ff33ff02ffff03ffff02ff2cffff04ff02ffff04ff8205ffffff01ff8080808080ffff01ff04ffff02ff14ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff82017fffff04ff81bfffff04ff8202ffff8080808080808080808080ff8205ff80ffff01ff088080ff0180ffff02ffff03ffff18ff8202ffffff010180ffff01ff02ffff03ffff07ff82017f80ffff01ff04ff10ffff04ffff0bffff0bff82027fffff02ff2effff04ff02ffff04ffff02ff3cffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff82057fff80808080808080ffff04ff05ffff04ff0bffff04ff82057fff80808080808080ff820b7f80ffff02ff2effff04ff02ffff04ffff02ff3cffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff81bfff80808080808080ffff04ff05ffff04ff0bffff04ff81bfff80808080808080ff8202ff80ff808080ffff01ff04ff10ffff04ffff0bffff0bff2fff17ff5f80ffff02ff2effff04ff02ffff04ffff02ff3cffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff81bfff80808080808080ffff04ff05ffff04ff0bffff04ff81bfff80808080808080ff8202ff80ff80808080ff0180ffff01ff088080ff0180ffff02ffff03ff05ffff01ff02ffff03ffff09ff11ff2880ffff01ff02ffff03ffff18ff59ffff010180ffff01ff02ffff03ff0bffff01ff0880ffff01ff02ff2cffff04ff02ffff04ff0dffff01ff018080808080ff0180ffff01ff02ff2cffff04ff02ffff04ff0dffff04ff0bff808080808080ff0180ffff01ff02ff2cffff04ff02ffff04ff0dffff04ff0bff808080808080ff0180ffff01ff010180ff0180ff02ff12ffff04ff02ffff04ff05ffff04ff07ff8080808080ffffff04ffff0102ffff04ffff04ffff0101ff0580ffff04ffff02ff2affff04ff02ffff04ff0bffff01ff0180808080ff80808080ffff02ffff03ff05ffff01ff04ffff0104ffff04ffff04ffff0101ff0980ffff04ffff02ff2affff04ff02ffff04ff0dffff04ff0bff8080808080ff80808080ffff010b80ff0180ff02ffff03ff0bffff01ff02ffff03ffff09ff05ff1380ffff01ff0101ffff01ff02ff3affff04ff02ffff04ff05ffff04ff1bff808080808080ff0180ff8080ff0180ffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff16ffff04ff02ffff04ff09ff80808080ffff02ff16ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff02ff3effff04ff02ffff04ff05ffff04ff07ff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ffff04ff0bff8080808080ffff02ff3effff04ff02ffff04ff0dffff04ff0bff808080808080ffff01ff02ffff03ffff02ff3affff04ff02ffff04ff05ffff04ff0bff8080808080ffff0105ffff01ff0bffff0101ff058080ff018080ff0180ff018080 \ No newline at end of file +ff02ffff01ff02ffff03ffff18ff2fffff010180ffff01ff02ff36ffff04ff02ffff04ff05ffff04ff17ffff04ffff02ff26ffff04ff02ffff04ff0bff80808080ffff04ff2fffff04ff0bffff04ff5fff808080808080808080ffff01ff088080ff0180ffff04ffff01ffffffff4602ff3304ffff0101ff02ffff02ffff03ff05ffff01ff02ff5cffff04ff02ffff04ff0dffff04ffff0bff2cffff0bff24ff3880ffff0bff2cffff0bff2cffff0bff24ff3480ff0980ffff0bff2cff0bffff0bff24ff8080808080ff8080808080ffff010b80ff0180ff02ffff03ff0bffff01ff02ff32ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ffff02ff2affff04ff02ffff04ffff02ffff03ffff09ff23ff2880ffff0181b3ff8080ff0180ff80808080ff80808080808080ffff01ff02ffff03ff17ff80ffff01ff088080ff018080ff0180ffffffff0bffff0bff17ffff02ff3affff04ff02ffff04ff09ffff04ff2fffff04ffff02ff26ffff04ff02ffff04ff05ff80808080ff808080808080ff5f80ff0bff81bf80ff02ffff03ffff20ffff22ff4fff178080ffff01ff02ff7effff04ff02ffff04ff6fffff04ffff04ffff02ffff03ff4fffff01ff04ff23ffff04ffff02ff3affff04ff02ffff04ff09ffff04ff53ffff04ffff02ff26ffff04ff02ffff04ff05ff80808080ff808080808080ffff04ff81b3ff80808080ffff011380ff0180ffff02ff7cffff04ff02ffff04ff05ffff04ff1bffff04ffff21ff4fff1780ff80808080808080ff8080808080ffff01ff088080ff0180ffff04ffff09ffff18ff05ffff010180ffff010180ffff09ff05ffff01818f8080ff0bff2cffff0bff24ff3080ffff0bff2cffff0bff2cffff0bff24ff3480ff0580ffff0bff2cffff02ff5cffff04ff02ffff04ff07ffff04ffff0bff24ff2480ff8080808080ffff0bff24ff8080808080ffffff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff26ffff04ff02ffff04ff09ff80808080ffff02ff26ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff02ff5effff04ff02ffff04ff05ffff04ff0bffff04ffff02ff3affff04ff02ffff04ff09ffff04ff17ffff04ffff02ff26ffff04ff02ffff04ff05ff80808080ff808080808080ffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff80808080808080808080ffff04ffff04ff20ffff04ff17ff808080ffff02ff7cffff04ff02ffff04ff05ffff04ffff02ff82017fffff04ffff04ffff04ff17ff2f80ffff04ffff04ff5fff81bf80ffff04ff0bff05808080ff8202ff8080ffff01ff80808080808080ffff02ff2effff04ff02ffff04ff05ffff04ff0bffff04ffff02ffff03ff3bffff01ff02ff22ffff04ff02ffff04ff05ffff04ff17ffff04ff13ffff04ff2bffff04ff5bffff04ff5fff808080808080808080ffff01ff02ffff03ffff09ff15ffff0bff13ff1dff2b8080ffff01ff0bff15ff17ff5f80ffff01ff088080ff018080ff0180ffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fff8080808080808080808080ff02ffff03ff05ffff011bffff010b80ff0180ff018080 \ No newline at end of file diff --git a/chia/wallet/puzzles/singleton_top_layer.clvm.hex.sha256tree b/chia/wallet/puzzles/singleton_top_layer.clvm.hex.sha256tree index e93bf2c57012..094ac5289141 100644 --- a/chia/wallet/puzzles/singleton_top_layer.clvm.hex.sha256tree +++ b/chia/wallet/puzzles/singleton_top_layer.clvm.hex.sha256tree @@ -1 +1 @@ -cb48893e85dfbcee15ceb21187cec39a31764381fd887d604f4727e8ff30e0b5 +24e044101e57b3d8c908b8a38ad57848afd29d3eecc439dba45f4412df4954fd diff --git a/chia/wallet/puzzles/singleton_top_layer.py b/chia/wallet/puzzles/singleton_top_layer.py new file mode 100644 index 000000000000..ce4f55a58c80 --- /dev/null +++ b/chia/wallet/puzzles/singleton_top_layer.py @@ -0,0 +1,188 @@ +from typing import List, Tuple + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.condition_opcodes import ConditionOpcode +from chia.types.coin_solution import CoinSolution +from chia.wallet.puzzles.load_clvm import load_clvm +from chia.wallet.lineage_proof import LineageProof +from chia.util.ints import uint64 +from chia.util.hash import std_hash + +SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") +SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() +P2_SINGLETON_MOD = load_clvm("p2_singleton.clvm") +P2_SINGLETON_OR_DELAYED_MOD = load_clvm("p2_singleton_or_delayed_puzhash.clvm") +SINGLETON_LAUNCHER = load_clvm("singleton_launcher.clvm") +SINGLETON_LAUNCHER_HASH = SINGLETON_LAUNCHER.get_tree_hash() +ESCAPE_VALUE = -113 +MELT_CONDITION = [ConditionOpcode.CREATE_COIN, 0, ESCAPE_VALUE] + + +# Given the parent and amount of the launcher coin, return the launcher coin +def generate_launcher_coin(coin: Coin, amount: uint64) -> Coin: + return Coin(coin.name(), SINGLETON_LAUNCHER_HASH, amount) + + +# Wrap inner puzzles that are not singleton specific to strip away "truths" +def adapt_inner_to_singleton(inner_puzzle: Program) -> Program: + # (a (q . inner_puzzle) (r 1)) + return Program.to([2, (1, inner_puzzle), [6, 1]]) + + +# Take standard coin and amount -> launch conditions & launcher coin solution +def launch_conditions_and_coinsol( + coin: Coin, + inner_puzzle: Program, + comment: List[Tuple[str, str]], + amount: uint64, +) -> Tuple[List[Program], CoinSolution]: + if (amount % 2) == 0: + raise ValueError("Coin amount cannot be even. Subtract one mojo.") + + launcher_coin = generate_launcher_coin(coin, amount) + curried_singleton = SINGLETON_MOD.curry( + (SINGLETON_MOD_HASH, (launcher_coin.name(), SINGLETON_LAUNCHER_HASH)), + inner_puzzle, + ) + + launcher_solution = Program.to( + [ + curried_singleton.get_tree_hash(), + amount, + comment, + ] + ) + create_launcher = Program.to( + [ + ConditionOpcode.CREATE_COIN, + SINGLETON_LAUNCHER_HASH, + amount, + ], + ) + assert_launcher_announcement = Program.to( + [ + ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, + std_hash(launcher_coin.name() + launcher_solution.get_tree_hash()), + ], + ) + + conditions = [create_launcher, assert_launcher_announcement] + + launcher_coin_solution = CoinSolution( + launcher_coin, + SINGLETON_LAUNCHER, + launcher_solution, + ) + + return conditions, launcher_coin_solution + + +# Take a coin solution, return a lineage proof for their child to use in spends +def lineage_proof_for_coinsol(coin_solution: CoinSolution) -> LineageProof: + parent_name = coin_solution.coin.parent_coin_info + + inner_puzzle_hash = None + if coin_solution.coin.puzzle_hash != SINGLETON_LAUNCHER_HASH: + full_puzzle = Program.from_bytes(bytes(coin_solution.puzzle_reveal)) + r = full_puzzle.uncurry() + if r is not None: + _, args = r + _, inner_puzzle = list(args.as_iter()) + inner_puzzle_hash = inner_puzzle.get_tree_hash() + + amount = coin_solution.coin.amount + + return LineageProof( + parent_name, + inner_puzzle_hash, + amount, + ) + + +# Return the puzzle reveal of a singleton with specific ID and innerpuz +def puzzle_for_singleton(launcher_id: bytes32, inner_puz: Program) -> Program: + return SINGLETON_MOD.curry( + (SINGLETON_MOD_HASH, (launcher_id, SINGLETON_LAUNCHER_HASH)), + inner_puz, + ) + + +# Return a solution to spend a singleton +def solution_for_singleton( + lineage_proof: LineageProof, + amount: uint64, + inner_solution: Program, +) -> Program: + if lineage_proof.inner_puzzle_hash is None: + parent_info = [ + lineage_proof.parent_name, + lineage_proof.amount, + ] + else: + parent_info = [ + lineage_proof.parent_name, + lineage_proof.inner_puzzle_hash, + lineage_proof.amount, + ] + + return Program.to([parent_info, amount, inner_solution]) + + +# Create a coin that a singleton can claim +def pay_to_singleton_puzzle(launcher_id: bytes32) -> Program: + return P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, SINGLETON_LAUNCHER_HASH) + + +# Create a coin that a singleton can claim or that can be sent to another puzzle after a specified time +def pay_to_singleton_or_delay_puzzle(launcher_id: bytes32, delay_time: uint64, delay_ph: bytes32) -> Program: + return P2_SINGLETON_OR_DELAYED_MOD.curry( + SINGLETON_MOD_HASH, + launcher_id, + SINGLETON_LAUNCHER_HASH, + delay_time, + delay_ph, + ) + + +# Solution for EITHER p2_singleton or the claiming spend case for p2_singleton_or_delayed_puzhash +def solution_for_p2_singleton(p2_singleton_coin: Coin, singleton_inner_puzhash: bytes32) -> Program: + return Program.to([singleton_inner_puzhash, p2_singleton_coin.name()]) + + +# Solution for the delayed spend case for p2_singleton_or_delayed_puzhash +def solution_for_p2_delayed_puzzle(output_amount: uint64) -> Program: + return Program.to([output_amount, []]) + + +# Get announcement conditions for singleton solution and full CoinSolution for the claimed coin +def claim_p2_singleton( + p2_singleton_coin: Coin, + singleton_inner_puzhash: bytes32, + launcher_id: bytes32, +) -> Tuple[Program, Program, CoinSolution]: + assertion = Program.to([ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, std_hash(p2_singleton_coin.name() + b"$")]) + announcement = Program.to([ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, p2_singleton_coin.name()]) + claim_coinsol = CoinSolution( + p2_singleton_coin, + pay_to_singleton_puzzle(launcher_id), + solution_for_p2_singleton(p2_singleton_coin, singleton_inner_puzhash), + ) + return assertion, announcement, claim_coinsol + + +# Get the CoinSolution for spending to a delayed puzzle +def spend_to_delayed_puzzle( + p2_singleton_coin: Coin, + output_amount: uint64, + launcher_id: bytes32, + delay_time: uint64, + delay_ph: bytes32, +) -> CoinSolution: + claim_coinsol = CoinSolution( + p2_singleton_coin, + pay_to_singleton_or_delay_puzzle(launcher_id, delay_time, delay_ph), + solution_for_p2_delayed_puzzle(output_amount), + ) + return claim_coinsol diff --git a/chia/wallet/puzzles/singleton_truths.clib b/chia/wallet/puzzles/singleton_truths.clib new file mode 100644 index 000000000000..fe673eedcb11 --- /dev/null +++ b/chia/wallet/puzzles/singleton_truths.clib @@ -0,0 +1,21 @@ +( + (defun-inline truth_data_to_truth_struct (my_id full_puzhash innerpuzhash my_amount lineage_proof singleton_struct) (c (c my_id full_puzhash) (c (c innerpuzhash my_amount) (c lineage_proof singleton_struct)))) + + (defun-inline my_id_truth (Truths) (f (f Truths))) + (defun-inline my_full_puzzle_hash_truth (Truths) (r (f Truths))) + (defun-inline my_inner_puzzle_hash_truth (Truths) (f (f (r Truths)))) + (defun-inline my_amount_truth (Truths) (r (f (r Truths)))) + (defun-inline my_lineage_proof_truth (Truths) (f (r (r Truths)))) + (defun-inline singleton_struct_truth (Truths) (r (r (r Truths)))) + + (defun-inline singleton_mod_hash_truth (Truths) (f (singleton_struct_truth Truths))) + (defun-inline singleton_launcher_id_truth (Truths) (f (r (singleton_struct_truth Truths)))) + (defun-inline singleton_launcher_puzzle_hash_truth (Truths) (f (r (r (singleton_struct_truth Truths))))) + + (defun-inline parent_info_for_lineage_proof (lineage_proof) (f lineage_proof)) + (defun-inline puzzle_hash_for_lineage_proof (lineage_proof) (f (r lineage_proof))) + (defun-inline amount_for_lineage_proof (lineage_proof) (f (r (r lineage_proof)))) + (defun-inline is_not_eve_proof (lineage_proof) (r (r lineage_proof))) + (defun-inline parent_info_for_eve_proof (lineage_proof) (f lineage_proof)) + (defun-inline amount_for_eve_proof (lineage_proof) (f (r lineage_proof))) +) \ No newline at end of file diff --git a/chia/wallet/puzzles/test_cc.py b/chia/wallet/puzzles/test_cc.py index d88a5d1b9a77..656c2d7620e8 100644 --- a/chia/wallet/puzzles/test_cc.py +++ b/chia/wallet/puzzles/test_cc.py @@ -18,7 +18,6 @@ from chia.wallet.cc_wallet.cc_utils import ( spend_bundle_for_spendable_ccs, spendable_cc_list_from_coin_solution, ) -from chia.wallet.cc_wallet.debug_spend_bundle import debug_spend_bundle from chia.wallet.puzzles.genesis_by_coin_id_with_0 import create_genesis_or_zero_coin_checker from chia.wallet.puzzles.genesis_by_puzzle_hash_with_0 import create_genesis_puzzle_or_zero_coin_checker @@ -124,8 +123,7 @@ def test_spend_through_n(mod_code, coin_checker_for_farmed_coin, n): puzzles_for_db = [cc_puzzle_for_inner_puzzle(mod_code, genesis_coin_checker, eve_inner_puzzle)] add_puzzles_to_puzzle_preimage_db(puzzles_for_db) - - debug_spend_bundle(spend_bundle) + spend_bundle.debug() ################################ @@ -147,7 +145,7 @@ def test_spend_through_n(mod_code, coin_checker_for_farmed_coin, n): [inner_puzzle_solution], ) - debug_spend_bundle(spend_bundle) + spend_bundle.debug() ################################ @@ -172,7 +170,7 @@ def test_spend_through_n(mod_code, coin_checker_for_farmed_coin, n): inner_solutions, ) - debug_spend_bundle(spend_bundle) + spend_bundle.debug() def test_spend_zero_coin(mod_code: Program, coin_checker_for_farmed_coin): @@ -222,7 +220,7 @@ def test_spend_zero_coin(mod_code: Program, coin_checker_for_farmed_coin): solution_for_pay_to_any([(wrapped_cc_puzzle_hash, eve_cc_spendable.coin.amount)]), ] spend_bundle = spend_bundle_for_spendable_ccs(mod_code, genesis_coin_checker, spendable_cc_list, inner_solutions) - debug_spend_bundle(spend_bundle) + spend_bundle.debug() def main(): diff --git a/chia/wallet/rl_wallet/rl_wallet.py b/chia/wallet/rl_wallet/rl_wallet.py index c718e2f19a68..ef5d395df7fd 100644 --- a/chia/wallet/rl_wallet/rl_wallet.py +++ b/chia/wallet/rl_wallet/rl_wallet.py @@ -634,7 +634,7 @@ class RLWallet: if self.rl_coin_record is None: raise ValueError("Rl coin record is None") - list_of_coinsolutions = [] + list_of_coin_solutions = [] self.rl_coin_record = await self._get_rl_coin_record() pubkey, secretkey = await self.get_keys(self.rl_coin_record.coin.puzzle_hash) # Spend wallet coin @@ -659,7 +659,7 @@ class RLWallet: signature = AugSchemeMPL.sign(secretkey, solution.get_tree_hash()) rl_spend = CoinSolution(self.rl_coin_record.coin, puzzle, solution) - list_of_coinsolutions.append(rl_spend) + list_of_coin_solutions.append(rl_spend) # Spend consolidating coin puzzle = rl_make_aggregation_puzzle(self.rl_coin_record.coin.puzzle_hash) @@ -670,10 +670,10 @@ class RLWallet: ) agg_spend = CoinSolution(consolidating_coin, puzzle, solution) - list_of_coinsolutions.append(agg_spend) + list_of_coin_solutions.append(agg_spend) aggsig = AugSchemeMPL.aggregate([signature]) - return SpendBundle(list_of_coinsolutions, aggsig) + return SpendBundle(list_of_coin_solutions, aggsig) def rl_get_aggregation_puzzlehash(self, wallet_puzzle): puzzle_hash = rl_make_aggregation_puzzle(wallet_puzzle).get_tree_hash() @@ -688,5 +688,5 @@ class RLWallet: await self.main_wallet.push_transaction(spend_bundle) async def push_transaction(self, tx: TransactionRecord) -> None: - """ Use this API to send transactions. """ + """Use this API to send transactions.""" await self.wallet_state_manager.add_pending_transaction(tx) diff --git a/chia/wallet/sign_coin_solutions.py b/chia/wallet/sign_coin_solutions.py index d2cfdd9ff78b..cb97f4547577 100644 --- a/chia/wallet/sign_coin_solutions.py +++ b/chia/wallet/sign_coin_solutions.py @@ -1,7 +1,8 @@ -from typing import Callable, List, Optional +import inspect +from typing import List, Any import blspy -from blspy import AugSchemeMPL, PrivateKey +from blspy import AugSchemeMPL from chia.types.coin_solution import CoinSolution from chia.types.spend_bundle import SpendBundle @@ -10,7 +11,7 @@ from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_fo async def sign_coin_solutions( coin_solutions: List[CoinSolution], - secret_key_for_public_key_f: Callable[[blspy.G1Element], Optional[PrivateKey]], + secret_key_for_public_key_f: Any, # Potentially awaitable function from G1Element => Optional[PrivateKey] additional_data: bytes, max_cost: int, ) -> SpendBundle: @@ -32,7 +33,10 @@ async def sign_coin_solutions( ): pk_list.append(pk) msg_list.append(msg) - secret_key = secret_key_for_public_key_f(pk) + if inspect.iscoroutinefunction(secret_key_for_public_key_f): + secret_key = await secret_key_for_public_key_f(pk) + else: + secret_key = secret_key_for_public_key_f(pk) if secret_key is None: e_msg = f"no secret key for {pk}" raise ValueError(e_msg) diff --git a/chia/wallet/trade_manager.py b/chia/wallet/trade_manager.py index 20878350f64f..1ddf676bbe26 100644 --- a/chia/wallet/trade_manager.py +++ b/chia/wallet/trade_manager.py @@ -34,8 +34,6 @@ from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet import Wallet from chia.wallet.wallet_coin_record import WalletCoinRecord -# from chia.wallet.cc_wallet.debug_spend_bundle import debug_spend_bundle - class TradeManager: wallet_state_manager: Any @@ -153,7 +151,7 @@ class TradeManager: self.log.warning(f"Trade with id: {trade.trade_id} failed at height: {height}") async def get_locked_coins(self, wallet_id: int = None) -> Dict[bytes32, WalletCoinRecord]: - """ Returns a dictionary of confirmed coins that are locked by a trade. """ + """Returns a dictionary of confirmed coins that are locked by a trade.""" all_pending = [] pending_accept = await self.get_offers_with_status(TradeStatus.PENDING_ACCEPT) pending_confirm = await self.get_offers_with_status(TradeStatus.PENDING_CONFIRM) @@ -185,7 +183,7 @@ class TradeManager: return record async def get_locked_coins_in_spend_bundle(self, bundle: SpendBundle) -> Dict[bytes32, WalletCoinRecord]: - """ Returns a list of coin records that are used in this SpendBundle""" + """Returns a list of coin records that are used in this SpendBundle""" result = {} removals = bundle.removals() for coin in removals: @@ -199,7 +197,7 @@ class TradeManager: await self.trade_store.set_status(trade_id, TradeStatus.CANCELED, False) async def cancel_pending_offer_safely(self, trade_id: bytes32): - """ This will create a transaction that includes coins that were offered""" + """This will create a transaction that includes coins that were offered""" self.log.info(f"Secure-Cancel pending offer with id trade_id {trade_id.hex()}") trade = await self.trade_store.get_trade_record(trade_id) if trade is None: @@ -533,7 +531,6 @@ class TradeManager: now = uint64(int(time.time())) if chia_spend_bundle is not None: spend_bundle = SpendBundle.aggregate([spend_bundle, chia_spend_bundle]) - # debug_spend_bundle(spend_bundle) if chia_discrepancy < 0: tx_record = TransactionRecord( confirmed_at_height=uint32(0), diff --git a/chia/wallet/transaction_record.py b/chia/wallet/transaction_record.py index 1e6bcf6b28d9..6209ab7c2ba4 100644 --- a/chia/wallet/transaction_record.py +++ b/chia/wallet/transaction_record.py @@ -45,7 +45,7 @@ class TransactionRecord(Streamable): # Note, transactions pending inclusion (pending) return false return False - def height_farmed(self, genesis_challenge) -> Optional[uint32]: + def height_farmed(self, genesis_challenge: bytes32) -> Optional[uint32]: if not self.confirmed: return None if self.type == TransactionType.FEE_REWARD or self.type == TransactionType.COINBASE_REWARD: diff --git a/chia/wallet/cc_wallet/debug_spend_bundle.py b/chia/wallet/util/debug_spend_bundle.py similarity index 58% rename from chia/wallet/cc_wallet/debug_spend_bundle.py rename to chia/wallet/util/debug_spend_bundle.py index ce40795203cf..14b75c375c36 100644 --- a/chia/wallet/cc_wallet/debug_spend_bundle.py +++ b/chia/wallet/util/debug_spend_bundle.py @@ -8,7 +8,6 @@ from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program, INFINITE_COST from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.condition_opcodes import ConditionOpcode -from chia.types.spend_bundle import SpendBundle from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.hash import std_hash @@ -41,7 +40,7 @@ def dump_coin(coin: Coin) -> str: return disassemble(coin_as_program(coin)) -def debug_spend_bundle(spend_bundle: SpendBundle) -> None: +def debug_spend_bundle(spend_bundle, agg_sig_additional_data=bytes([3] * 32)) -> None: """ Print a lot of useful information about a `SpendBundle` that might help with debugging its clvm. @@ -50,16 +49,25 @@ def debug_spend_bundle(spend_bundle: SpendBundle) -> None: pks = [] msgs = [] - created_announcements: List[List[bytes]] = [] - asserted_annoucements = [] + created_coin_announcements: List[List[bytes]] = [] + asserted_coin_announcements = [] + created_puzzle_announcements: List[List[bytes]] = [] + asserted_puzzle_announcements = [] print("=" * 80) for coin_solution in spend_bundle.coin_solutions: coin = coin_solution.coin - puzzle_reveal = coin_solution.puzzle_reveal - solution = coin_solution.solution + puzzle_reveal = Program.from_bytes(bytes(coin_solution.puzzle_reveal)) + solution = Program.from_bytes(bytes(coin_solution.solution)) coin_name = coin.name() + if puzzle_reveal.get_tree_hash() != coin_solution.coin.puzzle_hash: + print("*** BAD PUZZLE REVEAL") + print(f"{puzzle_reveal.get_tree_hash().hex()} vs {coin_solution.coin.puzzle_hash.hex()}") + print("*" * 80) + breakpoint() + continue + print(f"consuming coin {dump_coin(coin)}") print(f" with id {coin_name}") print() @@ -68,11 +76,11 @@ def debug_spend_bundle(spend_bundle: SpendBundle) -> None: if error: print(f"*** error {error}") elif conditions is not None: - for pk, m in pkm_pairs_for_conditions_dict(conditions, coin_name, bytes([3] * 32)): + for pk, m in pkm_pairs_for_conditions_dict(conditions, coin_name, agg_sig_additional_data): pks.append(pk) msgs.append(m) print() - r = puzzle_reveal.run_with_cost(INFINITE_COST, solution) + cost, r = puzzle_reveal.run_with_cost(INFINITE_COST, solution) # type: ignore print(disassemble(r)) print() if conditions and len(conditions) > 0: @@ -85,12 +93,19 @@ def debug_spend_bundle(spend_bundle: SpendBundle) -> None: if len(c.vars) == 2: as_prog = Program.to([c.opcode, c.vars[0], c.vars[1]]) print(f" {disassemble(as_prog)}") - created_announcements.extend( + created_coin_announcements.extend( [coin_name] + _.vars for _ in conditions.get(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, []) ) - asserted_annoucements.extend( + asserted_coin_announcements.extend( [_.vars[0].hex() for _ in conditions.get(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [])] ) + created_puzzle_announcements.extend( + [puzzle_reveal.get_tree_hash()] + _.vars + for _ in conditions.get(ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, []) + ) + asserted_puzzle_announcements.extend( + [_.vars[0].hex() for _ in conditions.get(ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT, [])] + ) print() else: print("(no output conditions generated)") @@ -123,30 +138,57 @@ def debug_spend_bundle(spend_bundle: SpendBundle) -> None: print(f" {dump_coin(coin)}") print(f" => created coin id {coin.name()}") - created_announcement_pairs = [(_, std_hash(b"".join(_)).hex()) for _ in created_announcements] - if created_announcements: - print("created announcements") - for announcement, hashed in sorted(created_announcement_pairs, key=lambda _: _[-1]): + created_coin_announcement_pairs = [(_, std_hash(b"".join(_)).hex()) for _ in created_coin_announcements] + if created_coin_announcement_pairs: + print("created coin announcements") + for announcement, hashed in sorted(created_coin_announcement_pairs, key=lambda _: _[-1]): as_hex = [f"0x{_.hex()}" for _ in announcement] print(f" {as_hex} =>\n {hashed}") - eor_announcements = sorted(set(_[-1] for _ in created_announcement_pairs) ^ set(asserted_annoucements)) + eor_coin_announcements = sorted( + set(_[-1] for _ in created_coin_announcement_pairs) ^ set(asserted_coin_announcements) + ) + + created_puzzle_announcement_pairs = [(_, std_hash(b"".join(_)).hex()) for _ in created_puzzle_announcements] + if created_puzzle_announcements: + print("created puzzle announcements") + for announcement, hashed in sorted(created_puzzle_announcement_pairs, key=lambda _: _[-1]): + as_hex = [f"0x{_.hex()}" for _ in announcement] + print(f" {as_hex} =>\n {hashed}") + + eor_puzzle_announcements = sorted( + set(_[-1] for _ in created_puzzle_announcement_pairs) ^ set(asserted_puzzle_announcements) + ) print() print() print(f"zero_coin_set = {sorted(zero_coin_set)}") print() - print(f"created announcements = {sorted([_[-1] for _ in created_announcement_pairs])}") - print() - print(f"asserted announcements = {sorted(asserted_annoucements)}") - print() - print(f"symdiff of announcements = {sorted(eor_announcements)}") - print() + if created_coin_announcement_pairs or asserted_coin_announcements: + print(f"created coin announcements = {sorted([_[-1] for _ in created_coin_announcement_pairs])}") + print() + print(f"asserted coin announcements = {sorted(asserted_coin_announcements)}") + print() + print(f"symdiff of coin announcements = {sorted(eor_coin_announcements)}") + print() + if created_puzzle_announcement_pairs or asserted_puzzle_announcements: + print(f"created puzzle announcements = {sorted([_[-1] for _ in created_puzzle_announcement_pairs])}") + print() + print(f"asserted puzzle announcements = {sorted(asserted_puzzle_announcements)}") + print() + print(f"symdiff of puzzle announcements = {sorted(eor_puzzle_announcements)}") + print() print() print("=" * 80) print() validates = AugSchemeMPL.aggregate_verify(pks, msgs, spend_bundle.aggregated_signature) print(f"aggregated signature check pass: {validates}") + print(f"pks: {pks}") + print(f"msgs: {[msg.hex() for msg in msgs]}") + print(f" msg_data: {[msg.hex()[:-128] for msg in msgs]}") + print(f" coin_ids: {[msg.hex()[-128:-64] for msg in msgs]}") + print(f" add_data: {[msg.hex()[-64:] for msg in msgs]}") + print(f"signature: {spend_bundle.aggregated_signature}") def solution_for_pay_to_any(puzzle_hash_amount_pairs: Tuple[bytes32, int]) -> Program: diff --git a/chia/wallet/util/trade_utils.py b/chia/wallet/util/trade_utils.py index 90111ca3d57d..75361e7d9f3c 100644 --- a/chia/wallet/util/trade_utils.py +++ b/chia/wallet/util/trade_utils.py @@ -25,7 +25,7 @@ def trade_status_ui_string(status: TradeStatus): def trade_record_to_dict(record: TradeRecord) -> Dict: - """ Convenience function to return only part of trade record we care about and show correct status to the ui""" + """Convenience function to return only part of trade record we care about and show correct status to the ui""" result = {} result["trade_id"] = record.trade_id.hex() result["sent"] = record.sent diff --git a/chia/wallet/util/wallet_types.py b/chia/wallet/util/wallet_types.py index a23661924d75..717b320dda19 100644 --- a/chia/wallet/util/wallet_types.py +++ b/chia/wallet/util/wallet_types.py @@ -12,3 +12,4 @@ class WalletType(IntEnum): COLOURED_COIN = 6 RECOVERABLE = 7 DISTRIBUTED_ID = 8 + POOLING_WALLET = 9 diff --git a/chia/wallet/wallet.py b/chia/wallet/wallet.py index 9124ab51d3e8..8107b77c9e11 100644 --- a/chia/wallet/wallet.py +++ b/chia/wallet/wallet.py @@ -179,7 +179,7 @@ class Wallet: dr = await self.wallet_state_manager.get_unused_derivation_record(self.id()) return puzzle_for_pk(bytes(dr.pubkey)) - async def get_puzzle_hash(self, new: bool): + async def get_puzzle_hash(self, new: bool) -> bytes32: if new: return await self.get_new_puzzlehash() else: @@ -190,18 +190,18 @@ class Wallet: return await self.get_new_puzzlehash() return record.puzzle_hash - async def get_new_puzzlehash(self) -> bytes32: - return (await self.wallet_state_manager.get_unused_derivation_record(self.id())).puzzle_hash + async def get_new_puzzlehash(self, in_transaction: bool = False) -> bytes32: + return (await self.wallet_state_manager.get_unused_derivation_record(self.id(), in_transaction)).puzzle_hash def make_solution( self, primaries: Optional[List[Dict[str, Any]]] = None, min_time=0, me=None, - coin_announcements: Optional[List[bytes32]] = None, - coin_announcements_to_assert: Optional[List[bytes32]] = None, - puzzle_announcements=None, - puzzle_announcements_to_assert=None, + coin_announcements: Optional[Set[bytes32]] = None, + coin_announcements_to_assert: Optional[Set[bytes32]] = None, + puzzle_announcements: Optional[Set[bytes32]] = None, + puzzle_announcements_to_assert: Optional[Set[bytes32]] = None, fee=0, ) -> Program: assert fee >= 0 @@ -292,6 +292,7 @@ class Wallet: coins: Set[Coin] = None, primaries_input: Optional[List[Dict[str, Any]]] = None, ignore_max_send_amount: bool = False, + announcements_to_consume: Set[Announcement] = None, ) -> List[CoinSolution]: """ Generates a unsigned transaction in form of List(Puzzle, Solutions) @@ -347,10 +348,15 @@ class Wallet: for primary in primaries: message_list.append(Coin(coin.name(), primary["puzzlehash"], primary["amount"]).name()) message: bytes32 = std_hash(b"".join(message_list)) - solution: Program = self.make_solution(primaries=primaries, fee=fee, coin_announcements=[message]) + solution: Program = self.make_solution( + primaries=primaries, + fee=fee, + coin_announcements={message}, + coin_announcements_to_assert=announcements_to_consume, + ) primary_announcement_hash = Announcement(coin.name(), message).name() else: - solution = self.make_solution(coin_announcements_to_assert=[primary_announcement_hash]) + solution = self.make_solution(coin_announcements_to_assert={primary_announcement_hash}) spends.append( CoinSolution( @@ -378,6 +384,7 @@ class Wallet: coins: Set[Coin] = None, primaries: Optional[List[Dict[str, bytes32]]] = None, ignore_max_send_amount: bool = False, + announcements_to_consume: Set[Announcement] = None, ) -> TransactionRecord: """ Use this to generate transaction. @@ -389,7 +396,7 @@ class Wallet: non_change_amount = uint64(amount + sum(p["amount"] for p in primaries)) transaction = await self._generate_unsigned_transaction( - amount, puzzle_hash, fee, origin_id, coins, primaries, ignore_max_send_amount + amount, puzzle_hash, fee, origin_id, coins, primaries, ignore_max_send_amount, announcements_to_consume ) assert len(transaction) > 0 diff --git a/chia/wallet/wallet_block_store.py b/chia/wallet/wallet_block_store.py index 46ed03bf3f50..60e9135af549 100644 --- a/chia/wallet/wallet_block_store.py +++ b/chia/wallet/wallet_block_store.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from typing import Dict, List, Optional, Tuple import aiosqlite @@ -5,13 +6,21 @@ import aiosqlite from chia.consensus.block_record import BlockRecord from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary +from chia.types.coin_solution import CoinSolution from chia.types.header_block import HeaderBlock from chia.util.db_wrapper import DBWrapper from chia.util.ints import uint32, uint64 from chia.util.lru_cache import LRUCache +from chia.util.streamable import Streamable, streamable from chia.wallet.block_record import HeaderBlockRecord +@dataclass(frozen=True) +@streamable +class AdditionalCoinSpends(Streamable): + coin_spends_list: List[CoinSolution] + + class WalletBlockStore: """ This object handles HeaderBlocks and Blocks stored in DB used by wallet. @@ -48,6 +57,10 @@ class WalletBlockStore: "block blob, sub_epoch_summary blob, is_peak tinyint)" ) + await self.db.execute( + "CREATE TABLE IF NOT EXISTS additional_coin_spends(header_hash text PRIMARY KEY, spends_list_blob blob)" + ) + # Height index so we can look up in order of height for sync purposes await self.db.execute("CREATE INDEX IF NOT EXISTS height on block_records(height)") @@ -62,7 +75,12 @@ class WalletBlockStore: await cursor_2.close() await self.db.commit() - async def add_block_record(self, header_block_record: HeaderBlockRecord, block_record: BlockRecord): + async def add_block_record( + self, + header_block_record: HeaderBlockRecord, + block_record: BlockRecord, + additional_coin_spends: List[CoinSolution], + ): """ Adds a block record to the database. This block record is assumed to be connected to the chain, but it may or may not be in the LCA path. @@ -103,9 +121,15 @@ class WalletBlockStore: False, ), ) - await cursor_2.close() + if len(additional_coin_spends) > 0: + blob: bytes = bytes(AdditionalCoinSpends(additional_coin_spends)) + cursor_3 = await self.db.execute( + "INSERT OR REPLACE INTO additional_coin_spends VALUES(?, ?)", (header_block_record.header_hash, blob) + ) + await cursor_3.close() + async def get_header_block_at(self, heights: List[uint32]) -> List[HeaderBlock]: if len(heights) == 0: return [] @@ -132,6 +156,18 @@ class WalletBlockStore: else: return None + async def get_additional_coin_spends(self, header_hash: bytes32) -> Optional[List[CoinSolution]]: + cursor = await self.db.execute( + "SELECT spends_list_blob from additional_coin_spends WHERE header_hash=?", (header_hash.hex(),) + ) + row = await cursor.fetchone() + await cursor.close() + if row is not None: + coin_spends: AdditionalCoinSpends = AdditionalCoinSpends.from_bytes(row[0]) + return coin_spends.coin_spends_list + else: + return None + async def get_block_record(self, header_hash: bytes32) -> Optional[BlockRecord]: cursor = await self.db.execute( "SELECT block from block_records WHERE header_hash=?", @@ -164,6 +200,9 @@ class WalletBlockStore: peak = header_hash return ret, peak + def rollback_cache_block(self, header_hash: bytes32): + self.block_cache.remove(header_hash) + async def set_peak(self, header_hash: bytes32) -> None: cursor_1 = await self.db.execute("UPDATE block_records SET is_peak=0 WHERE is_peak=1") await cursor_1.close() diff --git a/chia/wallet/wallet_blockchain.py b/chia/wallet/wallet_blockchain.py index 2f8a62ff270a..21441ec70441 100644 --- a/chia/wallet/wallet_blockchain.py +++ b/chia/wallet/wallet_blockchain.py @@ -16,6 +16,7 @@ from chia.consensus.full_block_to_block_record import block_to_block_record from chia.consensus.multiprocess_validation import PreValidationResult, pre_validate_blocks_multiprocessing from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.sub_epoch_summary import SubEpochSummary +from chia.types.coin_solution import CoinSolution from chia.types.header_block import HeaderBlock from chia.types.unfinished_header_block import UnfinishedHeaderBlock from chia.util.errors import Err, ValidationError @@ -24,6 +25,7 @@ from chia.util.streamable import recurse_jsonify from chia.wallet.block_record import HeaderBlockRecord from chia.wallet.wallet_block_store import WalletBlockStore from chia.wallet.wallet_coin_store import WalletCoinStore +from chia.wallet.wallet_pool_store import WalletPoolStore from chia.wallet.wallet_transaction_store import WalletTransactionStore log = logging.getLogger(__name__) @@ -57,15 +59,15 @@ class WalletBlockchain(BlockchainInterface): # All sub-epoch summaries that have been included in the blockchain from the beginning until and including the peak # (height_included, SubEpochSummary). Note: ONLY for the blocks in the path to the peak __sub_epoch_summaries: Dict[uint32, SubEpochSummary] = {} - # Unspent Store + # Stores coin_store: WalletCoinStore tx_store: WalletTransactionStore - # Store + pool_store: WalletPoolStore block_store: WalletBlockStore # Used to verify blocks in parallel pool: ProcessPoolExecutor - coins_of_interest_received: Any + new_transaction_block_callback: Any reorg_rollback: Any wallet_state_manager_lock: asyncio.Lock @@ -81,8 +83,9 @@ class WalletBlockchain(BlockchainInterface): block_store: WalletBlockStore, coin_store: WalletCoinStore, tx_store: WalletTransactionStore, + pool_store: WalletPoolStore, consensus_constants: ConsensusConstants, - coins_of_interest_received: Callable, # f(removals: List[Coin], additions: List[Coin], height: uint32) + new_transaction_block_callback: Callable, # f(removals: List[Coin], additions: List[Coin], height: uint32) reorg_rollback: Callable, lock: asyncio.Lock, ): @@ -95,6 +98,7 @@ class WalletBlockchain(BlockchainInterface): self.lock = asyncio.Lock() self.coin_store = coin_store self.tx_store = tx_store + self.pool_store = pool_store cpu_count = multiprocessing.cpu_count() if cpu_count > 61: cpu_count = 61 # Windows Server 2016 has an issue https://bugs.python.org/issue26903 @@ -105,7 +109,7 @@ class WalletBlockchain(BlockchainInterface): self.constants_json = recurse_jsonify(dataclasses.asdict(self.constants)) self.block_store = block_store self._shut_down = False - self.coins_of_interest_received = coins_of_interest_received + self.new_transaction_block_callback = new_transaction_block_callback self.reorg_rollback = reorg_rollback self.log = logging.getLogger(__name__) self.wallet_state_manager_lock = lock @@ -152,6 +156,7 @@ class WalletBlockchain(BlockchainInterface): pre_validation_result: Optional[PreValidationResult] = None, trusted: bool = False, fork_point_with_peak: Optional[uint32] = None, + additional_coin_spends: List[CoinSolution] = None, ) -> Tuple[ReceiveBlockResult, Optional[Err], Optional[uint32]]: """ Adds a new block into the blockchain, if it's valid and connected to the current @@ -160,6 +165,8 @@ class WalletBlockchain(BlockchainInterface): invalid. Also returns the fork height, in the case of a new peak. """ + if additional_coin_spends is None: + additional_coin_spends = [] block = header_block_record.header genesis: bool = block.height == 0 @@ -218,26 +225,37 @@ class WalletBlockchain(BlockchainInterface): None, block, ) - + heights_changed: Set[Tuple[uint32, Optional[bytes32]]] = set() # Always add the block to the database async with self.wallet_state_manager_lock: async with self.block_store.db_wrapper.lock: try: await self.block_store.db_wrapper.begin_transaction() - await self.block_store.add_block_record(header_block_record, block_record) + await self.block_store.add_block_record(header_block_record, block_record, additional_coin_spends) self.add_block_record(block_record) self.clean_block_record(block_record.height - self.constants.BLOCKS_CACHE_SIZE) - - fork_height: Optional[uint32] = await self._reconsider_peak( - block_record, genesis, fork_point_with_peak + fork_height, records_to_add = await self._reconsider_peak( + block_record, genesis, fork_point_with_peak, additional_coin_spends, heights_changed ) + for record in records_to_add: + if record.sub_epoch_summary_included is not None: + self.__sub_epoch_summaries[record.height] = record.sub_epoch_summary_included await self.block_store.db_wrapper.commit_transaction() except BaseException as e: self.log.error(f"Error during db transaction: {e}") if self.block_store.db_wrapper.db._connection is not None: await self.block_store.db_wrapper.rollback_transaction() + self.remove_block_record(block_record.header_hash) + self.block_store.rollback_cache_block(block_record.header_hash) await self.coin_store.rebuild_wallet_cache() await self.tx_store.rebuild_tx_cache() + await self.pool_store.rebuild_cache() + for height, replaced in heights_changed: + # If it was replaced change back to the previous value otherwise pop the change + if replaced is not None: + self.__height_to_hash[height] = replaced + else: + self.__height_to_hash.pop(height) raise if fork_height is not None: self.log.info(f"💰 Updated wallet peak to height {block_record.height}, weight {block_record.weight}, ") @@ -246,8 +264,13 @@ class WalletBlockchain(BlockchainInterface): return ReceiveBlockResult.ADDED_AS_ORPHAN, None, None async def _reconsider_peak( - self, block_record: BlockRecord, genesis: bool, fork_point_with_peak: Optional[uint32] - ) -> Optional[uint32]: + self, + block_record: BlockRecord, + genesis: bool, + fork_point_with_peak: Optional[uint32], + additional_coin_spends_from_wallet: Optional[List[CoinSolution]], + heights_changed: Set[Tuple[uint32, Optional[bytes32]]], + ) -> Tuple[Optional[uint32], List[BlockRecord]]: """ When a new block is added, this is called, to check if the new block is the new peak of the chain. This also handles reorgs by reverting blocks which are not in the heaviest chain. @@ -261,13 +284,16 @@ class WalletBlockchain(BlockchainInterface): block_record.header_hash ) assert block is not None + replaced = None + if uint32(0) in self.__height_to_hash: + replaced = (self.__height_to_hash[uint32(0)],) self.__height_to_hash[uint32(0)] = block.header_hash - for removed in block.removals: - self.log.debug(f"Removed: {removed.name()}") - await self.coins_of_interest_received(block.removals, block.additions, block.height) + heights_changed.add((uint32(0), replaced)) + assert len(block.additions) == 0 and len(block.removals) == 0 + await self.new_transaction_block_callback(block.removals, block.additions, block_record, []) self._peak_height = uint32(0) - return uint32(0) - return None + return uint32(0), [block_record] + return None, [] assert peak is not None if block_record.weight > peak.weight: @@ -292,41 +318,50 @@ class WalletBlockchain(BlockchainInterface): del self.__sub_epoch_summaries[height] # Collect all blocks from fork point to new peak - blocks_to_add: List[Tuple[HeaderBlockRecord, BlockRecord]] = [] + blocks_to_add: List[Tuple[HeaderBlockRecord, BlockRecord, List[CoinSolution]]] = [] curr = block_record.header_hash while fork_h < 0 or curr != self.height_to_hash(uint32(fork_h)): fetched_header_block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record(curr) fetched_block_record: Optional[BlockRecord] = await self.block_store.get_block_record(curr) + if curr == block_record.header_hash: + additional_coin_spends = additional_coin_spends_from_wallet + else: + additional_coin_spends = await self.block_store.get_additional_coin_spends(curr) + if additional_coin_spends is None: + additional_coin_spends = [] assert fetched_header_block is not None assert fetched_block_record is not None - blocks_to_add.append((fetched_header_block, fetched_block_record)) + blocks_to_add.append((fetched_header_block, fetched_block_record, additional_coin_spends)) if fetched_header_block.height == 0: # Doing a full reorg, starting at height 0 break curr = fetched_block_record.prev_hash - for fetched_header_block, fetched_block_record in reversed(blocks_to_add): + records_to_add: List[BlockRecord] = [] + for fetched_header_block, fetched_block_record, additional_coin_spends in reversed(blocks_to_add): + replaced = None + if fetched_block_record.height in self.__height_to_hash: + replaced = self.__height_to_hash[fetched_block_record.height] self.__height_to_hash[fetched_block_record.height] = fetched_block_record.header_hash + heights_changed.add((fetched_block_record.height, replaced)) + records_to_add.append(fetched_block_record) if fetched_block_record.is_transaction_block: - await self.coins_of_interest_received( + await self.new_transaction_block_callback( fetched_header_block.removals, fetched_header_block.additions, - fetched_header_block.height, + fetched_block_record, + additional_coin_spends, ) - if fetched_block_record.sub_epoch_summary_included is not None: - self.__sub_epoch_summaries[ - fetched_block_record.height - ] = fetched_block_record.sub_epoch_summary_included # Changes the peak to be the new peak await self.block_store.set_peak(block_record.header_hash) self._peak_height = block_record.height if fork_h < 0: - return None - return uint32(fork_h) + return None, records_to_add + return uint32(fork_h), records_to_add # This is not a heavier block than the heaviest we have seen, so we don't change the coin set - return None + return None, [] def get_next_difficulty(self, header_hash: bytes32, new_slot: bool) -> uint64: assert self.contains_block(header_hash) diff --git a/chia/wallet/wallet_coin_store.py b/chia/wallet/wallet_coin_store.py index a52e9d6e7ce3..9fc7a956ff43 100644 --- a/chia/wallet/wallet_coin_store.py +++ b/chia/wallet/wallet_coin_store.py @@ -143,7 +143,7 @@ class WalletCoinStore: ) async def get_coin_record(self, coin_name: bytes32) -> Optional[WalletCoinRecord]: - """ Returns CoinRecord with specified coin id. """ + """Returns CoinRecord with specified coin id.""" if coin_name in self.coin_record_cache: return self.coin_record_cache[coin_name] cursor = await self.db_connection.execute("SELECT * from coin_record WHERE coin_name=?", (coin_name.hex(),)) @@ -155,7 +155,7 @@ class WalletCoinStore: return self.coin_record_from_row(row) async def get_first_coin_height(self) -> Optional[uint32]: - """ Returns height of first confirmed coin""" + """Returns height of first confirmed coin""" cursor = await self.db_connection.execute("SELECT MIN(confirmed_height) FROM coin_record;") row = await cursor.fetchone() await cursor.close() @@ -188,7 +188,7 @@ class WalletCoinStore: return all_unspent async def get_unspent_coins_for_wallet(self, wallet_id: int) -> Set[WalletCoinRecord]: - """ Returns set of CoinRecords that have not been spent yet for a wallet. """ + """Returns set of CoinRecords that have not been spent yet for a wallet.""" if wallet_id in self.unspent_coin_wallet_cache: wallet_coins: Dict[bytes32, WalletCoinRecord] = self.unspent_coin_wallet_cache[wallet_id] return set(wallet_coins.values()) @@ -196,7 +196,7 @@ class WalletCoinStore: return set() async def get_all_coins(self) -> Set[WalletCoinRecord]: - """ Returns set of all CoinRecords.""" + """Returns set of all CoinRecords.""" cursor = await self.db_connection.execute("SELECT * from coin_record") rows = await cursor.fetchall() await cursor.close() diff --git a/chia/wallet/wallet_interested_store.py b/chia/wallet/wallet_interested_store.py new file mode 100644 index 000000000000..5324523f98cc --- /dev/null +++ b/chia/wallet/wallet_interested_store.py @@ -0,0 +1,101 @@ +from typing import List, Tuple, Optional + +import aiosqlite + +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.db_wrapper import DBWrapper + + +class WalletInterestedStore: + """ + Stores coin ids that we are interested in receiving + """ + + db_connection: aiosqlite.Connection + db_wrapper: DBWrapper + + @classmethod + async def create(cls, wrapper: DBWrapper): + self = cls() + + self.db_connection = wrapper.db + self.db_wrapper = wrapper + await self.db_connection.execute("pragma journal_mode=wal") + await self.db_connection.execute("pragma synchronous=2") + + await self.db_connection.execute("CREATE TABLE IF NOT EXISTS interested_coins(coin_name text PRIMARY KEY)") + + await self.db_connection.execute( + "CREATE TABLE IF NOT EXISTS interested_puzzle_hashes(puzzle_hash text PRIMARY KEY, wallet_id integer)" + ) + await self.db_connection.commit() + return self + + async def _clear_database(self): + cursor = await self.db_connection.execute("DELETE FROM puzzle_hashes") + await cursor.close() + cursor = await self.db_connection.execute("DELETE FROM interested_coins") + await cursor.close() + await self.db_connection.commit() + + async def get_interested_coin_ids(self) -> List[bytes32]: + cursor = await self.db_connection.execute("SELECT coin_name FROM interested_coins") + rows_hex = await cursor.fetchall() + return [bytes32(bytes.fromhex(row[0])) for row in rows_hex] + + async def add_interested_coin_id(self, coin_id: bytes32, in_transaction: bool = False) -> None: + + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute( + "INSERT OR REPLACE INTO interested_coins VALUES (?)", (coin_id.hex(),) + ) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() + + async def get_interested_puzzle_hashes(self) -> List[Tuple[bytes32, int]]: + cursor = await self.db_connection.execute("SELECT puzzle_hash, wallet_id FROM interested_puzzle_hashes") + rows_hex = await cursor.fetchall() + return [(bytes32(bytes.fromhex(row[0])), row[1]) for row in rows_hex] + + async def get_interested_puzzle_hash_wallet_id(self, puzzle_hash: bytes32) -> Optional[int]: + cursor = await self.db_connection.execute( + "SELECT wallet_id FROM interested_puzzle_hashes WHERE puzzle_hash=?", (puzzle_hash.hex(),) + ) + row = await cursor.fetchone() + if row is None: + return None + return row[0] + + async def add_interested_puzzle_hash( + self, puzzle_hash: bytes32, wallet_id: int, in_transaction: bool = False + ) -> None: + + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute( + "INSERT OR REPLACE INTO interested_puzzle_hashes VALUES (?, ?)", (puzzle_hash.hex(), wallet_id) + ) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() + + async def remove_interested_puzzle_hash(self, puzzle_hash: bytes32, in_transaction: bool = False) -> None: + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: + cursor = await self.db_connection.execute( + "DELETE FROM interested_puzzle_hashes WHERE puzzle_hash=?", (puzzle_hash.hex(),) + ) + await cursor.close() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() diff --git a/chia/wallet/wallet_node.py b/chia/wallet/wallet_node.py index 7a2bd4158afc..a00d59a78396 100644 --- a/chia/wallet/wallet_node.py +++ b/chia/wallet/wallet_node.py @@ -12,6 +12,7 @@ from blspy import PrivateKey from chia.consensus.block_record import BlockRecord from chia.consensus.constants import ConsensusConstants from chia.consensus.multiprocess_validation import PreValidationResult +from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH from chia.protocols import wallet_protocol from chia.protocols.full_node_protocol import RequestProofOfWeight, RespondProofOfWeight from chia.protocols.protocol_message_types import ProtocolMessageTypes @@ -31,7 +32,9 @@ from chia.server.server import ChiaServer from chia.server.ws_connection import WSChiaConnection from chia.types.blockchain_format.coin import Coin, hash_coin_list from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_solution import CoinSolution from chia.types.header_block import HeaderBlock +from chia.types.mempool_inclusion_status import MempoolInclusionStatus from chia.types.peer_info import PeerInfo from chia.util.byte_types import hexstr_to_bytes from chia.util.errors import Err, ValidationError @@ -154,7 +157,7 @@ class WalletNode: assert self.server is not None self.wallet_state_manager = await WalletStateManager.create( - private_key, self.config, path, self.constants, self.server + private_key, self.config, path, self.constants, self.server, self.root_path ) self.wsm_close_task = None @@ -310,7 +313,8 @@ class WalletNode: ) already_sent = set() for peer, status, _ in record.sent_to: - already_sent.add(hexstr_to_bytes(peer)) + if status == MempoolInclusionStatus.SUCCESS.value: + already_sent.add(hexstr_to_bytes(peer)) messages.append((msg, already_sent)) return messages @@ -400,20 +404,26 @@ class WalletNode: removed_coins = await self.get_removals(peer, block, added_coins, removals) if removed_coins is None: raise ValueError("Failed to fetch removals") + + # If there is a launcher created, or we have a singleton spent, fetches the required solutions + additional_coin_spends: List[CoinSolution] = await self.get_additional_coin_spends( + peer, block, added_coins, removed_coins + ) + hbr = HeaderBlockRecord(block, added_coins, removed_coins) else: hbr = HeaderBlockRecord(block, [], []) header_block_records.append(hbr) - ( - result, - error, - fork_h, - ) = await self.wallet_state_manager.blockchain.receive_block(hbr, trusted=trusted) + additional_coin_spends = [] + (result, error, fork_h,) = await self.wallet_state_manager.blockchain.receive_block( + hbr, trusted=trusted, additional_coin_spends=additional_coin_spends + ) if result == ReceiveBlockResult.NEW_PEAK: if not self.wallet_state_manager.sync_mode: self.wallet_state_manager.blockchain.clean_block_records() self.wallet_state_manager.state_changed("new_block") self.wallet_state_manager.state_changed("sync_changed") + await self.wallet_state_manager.new_peak() elif result == ReceiveBlockResult.INVALID_BLOCK: self.log.info(f"Invalid block from peer: {peer.get_peer_info()} {error}") await peer.close() @@ -705,18 +715,32 @@ class WalletNode: if removed_coins is None: raise ValueError("Failed to fetch removals") + # If there is a launcher created, or we have a singleton spent, fetches the required solutions + additional_coin_spends: List[CoinSolution] = await self.get_additional_coin_spends( + peer, header_block, added_coins, removed_coins + ) + header_block_record = HeaderBlockRecord(header_block, added_coins, removed_coins) else: header_block_record = HeaderBlockRecord(header_block, [], []) + additional_coin_spends = [] start_t = time.time() if trusted: (result, error, fork_h,) = await self.wallet_state_manager.blockchain.receive_block( - header_block_record, None, trusted, fork_point_with_old_peak + header_block_record, + None, + trusted, + fork_point_with_old_peak, + additional_coin_spends=additional_coin_spends, ) else: assert pre_validation_results is not None (result, error, fork_h,) = await self.wallet_state_manager.blockchain.receive_block( - header_block_record, pre_validation_results[i], trusted, fork_point_with_old_peak + header_block_record, + pre_validation_results[i], + trusted, + fork_point_with_old_peak, + additional_coin_spends=additional_coin_spends, ) self.log.debug( f"Time taken to validate {header_block.height} with fork " @@ -837,8 +861,65 @@ class WalletNode: return False return True - async def get_additions(self, peer: WSChiaConnection, block_i, additions) -> Optional[List[Coin]]: - if len(additions) > 0: + async def fetch_puzzle_solution(self, peer, height: uint32, coin: Coin) -> CoinSolution: + solution_response = await peer.request_puzzle_solution( + wallet_protocol.RequestPuzzleSolution(coin.name(), height) + ) + if solution_response is None or not isinstance(solution_response, wallet_protocol.RespondPuzzleSolution): + raise ValueError(f"Was not able to obtain solution {solution_response}") + return CoinSolution(coin, solution_response.response.puzzle, solution_response.response.solution) + + async def get_additional_coin_spends( + self, peer, block, added_coins: List[Coin], removed_coins: List[Coin] + ) -> List[CoinSolution]: + assert self.wallet_state_manager is not None + additional_coin_spends: List[CoinSolution] = [] + if len(removed_coins) > 0: + removed_coin_ids = set([coin.name() for coin in removed_coins]) + all_added_coins = await self.get_additions(peer, block, [], get_all_additions=True) + assert all_added_coins is not None + if all_added_coins is not None: + + for coin in all_added_coins: + # This searches specifically for a launcher being created, and adds the solution of the launcher + if coin.puzzle_hash == SINGLETON_LAUNCHER_HASH and coin.parent_coin_info in removed_coin_ids: + cs: CoinSolution = await self.fetch_puzzle_solution(peer, block.height, coin) + additional_coin_spends.append(cs) + # Apply this coin solution, which might add things to interested list + await self.wallet_state_manager.get_next_interesting_coin_ids(cs, False) + + all_removed_coins: Optional[List[Coin]] = await self.get_removals( + peer, block, added_coins, removed_coins, request_all_removals=True + ) + assert all_removed_coins is not None + all_removed_coins_dict: Dict[bytes32, Coin] = {coin.name(): coin for coin in all_removed_coins} + keep_searching = True + while keep_searching: + # This keeps fetching solutions for coins we are interested list, in this block, until + # there are no more interested things to fetch + keep_searching = False + interested_ids: List[ + bytes32 + ] = await self.wallet_state_manager.interested_store.get_interested_coin_ids() + for coin_id in interested_ids: + if coin_id in all_removed_coins_dict: + coin = all_removed_coins_dict[coin_id] + cs = await self.fetch_puzzle_solution(peer, block.height, coin) + + # Apply this coin solution, which might add things to interested list + await self.wallet_state_manager.get_next_interesting_coin_ids(cs, False) + additional_coin_spends.append(cs) + keep_searching = True + all_removed_coins_dict.pop(coin_id) + break + return additional_coin_spends + + async def get_additions( + self, peer: WSChiaConnection, block_i, additions: Optional[List[bytes32]], get_all_additions: bool = False + ) -> Optional[List[Coin]]: + if (additions is not None and len(additions) > 0) or get_all_additions: + if get_all_additions: + additions = None additions_request = RequestAdditions(block_i.height, block_i.header_hash, additions) additions_res: Optional[Union[RespondAdditions, RejectAdditionsRequest]] = await peer.request_additions( additions_request @@ -867,9 +948,10 @@ class WalletNode: else: return [] # No added coins - async def get_removals(self, peer: WSChiaConnection, block_i, additions, removals) -> Optional[List[Coin]]: + async def get_removals( + self, peer: WSChiaConnection, block_i, additions, removals, request_all_removals=False + ) -> Optional[List[Coin]]: assert self.wallet_state_manager is not None - request_all_removals = False # Check if we need all removals for coin in additions: puzzle_store = self.wallet_state_manager.puzzle_store @@ -883,7 +965,6 @@ class WalletNode: if record_info is not None and record_info.wallet_type == WalletType.DISTRIBUTED_ID: request_all_removals = True break - if len(removals) > 0 or request_all_removals: if request_all_removals: removals_request = wallet_protocol.RequestRemovals(block_i.height, block_i.header_hash, None) diff --git a/chia/wallet/wallet_pool_store.py b/chia/wallet/wallet_pool_store.py new file mode 100644 index 000000000000..33d0955971b7 --- /dev/null +++ b/chia/wallet/wallet_pool_store.py @@ -0,0 +1,117 @@ +import logging +from typing import List, Tuple, Dict, Optional + +import aiosqlite + +from chia.types.coin_solution import CoinSolution +from chia.util.db_wrapper import DBWrapper +from chia.util.ints import uint32 + +log = logging.getLogger(__name__) + + +class WalletPoolStore: + db_connection: aiosqlite.Connection + db_wrapper: DBWrapper + _state_transitions_cache: Dict[int, List[Tuple[uint32, CoinSolution]]] + + @classmethod + async def create(cls, wrapper: DBWrapper): + self = cls() + + self.db_connection = wrapper.db + self.db_wrapper = wrapper + await self.db_connection.execute("pragma journal_mode=wal") + await self.db_connection.execute("pragma synchronous=2") + + await self.db_connection.execute( + "CREATE TABLE IF NOT EXISTS pool_state_transitions(transition_index integer, wallet_id integer, " + "height bigint, coin_spend blob, PRIMARY KEY(transition_index, wallet_id))" + ) + await self.db_connection.commit() + await self.rebuild_cache() + return self + + async def _clear_database(self): + cursor = await self.db_connection.execute("DELETE FROM interested_coins") + await cursor.close() + await self.db_connection.commit() + + async def add_spend( + self, + wallet_id: int, + spend: CoinSolution, + height: uint32, + ) -> None: + """ + Appends (or replaces) entries in the DB. The new list must be at least as long as the existing list, and the + parent of the first spend must already be present in the DB. Note that this is not committed to the DB + until db_wrapper.commit() is called. However it is written to the cache, so it can be fetched with + get_all_state_transitions. + """ + if wallet_id not in self._state_transitions_cache: + self._state_transitions_cache[wallet_id] = [] + all_state_transitions: List[Tuple[uint32, CoinSolution]] = self.get_spends_for_wallet(wallet_id) + + if (height, spend) in all_state_transitions: + return + + if len(all_state_transitions) > 0: + if height < all_state_transitions[-1][0]: + raise ValueError("Height cannot go down") + if spend.coin.parent_coin_info != all_state_transitions[-1][1].coin.name(): + raise ValueError("New spend does not extend") + + all_state_transitions.append((height, spend)) + + cursor = await self.db_connection.execute( + "INSERT OR REPLACE INTO pool_state_transitions VALUES (?, ?, ?, ?)", + ( + len(all_state_transitions) - 1, + wallet_id, + height, + bytes(spend), + ), + ) + await cursor.close() + + def get_spends_for_wallet(self, wallet_id: int) -> List[Tuple[uint32, CoinSolution]]: + """ + Retrieves all entries for a wallet ID from the cache, works even if commit is not called yet. + """ + return self._state_transitions_cache.get(wallet_id, []) + + async def rebuild_cache(self) -> None: + """ + This resets the cache, and loads all entries from the DB. Any entries in the cache that were not committed + are removed. This can happen if a state transition in wallet_blockchain fails. + """ + cursor = await self.db_connection.execute("SELECT * FROM pool_state_transitions ORDER BY transition_index") + rows = await cursor.fetchall() + await cursor.close() + self._state_transitions_cache = {} + for row in rows: + _, wallet_id, height, coin_solution_bytes = row + coin_solution: CoinSolution = CoinSolution.from_bytes(coin_solution_bytes) + if wallet_id not in self._state_transitions_cache: + self._state_transitions_cache[wallet_id] = [] + self._state_transitions_cache[wallet_id].append((height, coin_solution)) + + async def rollback(self, height: int, wallet_id_arg: int) -> None: + """ + Rollback removes all entries which have entry_height > height passed in. Note that this is not committed to the + DB until db_wrapper.commit() is called. However it is written to the cache, so it can be fetched with + get_all_state_transitions. + """ + for wallet_id, items in self._state_transitions_cache.items(): + remove_index_start: Optional[int] = None + for i, (item_block_height, _) in enumerate(items): + if item_block_height > height and wallet_id == wallet_id_arg: + remove_index_start = i + break + if remove_index_start is not None: + del items[remove_index_start:] + cursor = await self.db_connection.execute( + "DELETE FROM pool_state_transitions WHERE height>? AND wallet_id=?", (height, wallet_id_arg) + ) + await cursor.close() diff --git a/chia/wallet/wallet_puzzle_store.py b/chia/wallet/wallet_puzzle_store.py index 7909a8382b0d..fd1e2140dd7a 100644 --- a/chia/wallet/wallet_puzzle_store.py +++ b/chia/wallet/wallet_puzzle_store.py @@ -17,6 +17,8 @@ log = logging.getLogger(__name__) class WalletPuzzleStore: """ WalletPuzzleStore keeps track of all generated puzzle_hashes and their derivation path / wallet. + This is only used for HD wallets where each address is derived from a public key. Otherwise, use the + WalletInterestedStore to keep track of puzzle hashes which we are interested in. """ db_connection: aiosqlite.Connection @@ -77,11 +79,14 @@ class WalletPuzzleStore: await cursor.close() await self.db_connection.commit() - async def add_derivation_paths(self, records: List[DerivationRecord]) -> None: + async def add_derivation_paths(self, records: List[DerivationRecord], in_transaction=False) -> None: """ Insert many derivation paths into the database. """ - async with self.db_wrapper.lock: + + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: sql_records = [] for record in records: self.all_puzzle_hashes.add(record.puzzle_hash) @@ -102,7 +107,10 @@ class WalletPuzzleStore: ) await cursor.close() - await self.db_connection.commit() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() async def get_derivation_record(self, index: uint32, wallet_id: uint32) -> Optional[DerivationRecord]: """ diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 02ef23df9812..667933fd7d01 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -18,10 +18,13 @@ from chia.consensus.coinbase import pool_parent_id, farmer_parent_id from chia.consensus.constants import ConsensusConstants from chia.consensus.find_fork_point import find_fork_point_in_chain from chia.full_node.weight_proof import WeightProofHandler +from chia.pools.pool_puzzles import SINGLETON_LAUNCHER_HASH, solution_to_extra_data +from chia.pools.pool_wallet import PoolWallet from chia.protocols.wallet_protocol import PuzzleSolutionResponse, RespondPuzzleSolution from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_solution import CoinSolution from chia.types.full_block import FullBlock from chia.types.header_block import HeaderBlock from chia.types.mempool_inclusion_status import MempoolInclusionStatus @@ -50,6 +53,8 @@ from chia.wallet.wallet_blockchain import WalletBlockchain from chia.wallet.wallet_coin_record import WalletCoinRecord from chia.wallet.wallet_coin_store import WalletCoinStore from chia.wallet.wallet_info import WalletInfo, WalletInfoBackup +from chia.wallet.wallet_interested_store import WalletInterestedStore +from chia.wallet.wallet_pool_store import WalletPoolStore from chia.wallet.wallet_puzzle_store import WalletPuzzleStore from chia.wallet.wallet_sync_store import WalletSyncStore from chia.wallet.wallet_transaction_store import WalletTransactionStore @@ -83,6 +88,7 @@ class WalletStateManager: state_changed_callback: Optional[Callable] pending_tx_callback: Optional[Callable] puzzle_hash_created_callbacks: Dict = defaultdict(lambda *x: None) + new_peak_callbacks: Dict = defaultdict(lambda *x: None) db_path: Path db_connection: aiosqlite.Connection db_wrapper: DBWrapper @@ -98,8 +104,11 @@ class WalletStateManager: block_store: WalletBlockStore coin_store: WalletCoinStore sync_store: WalletSyncStore + interested_store: WalletInterestedStore + pool_store: WalletPoolStore weight_proof_handler: Any server: ChiaServer + root_path: Path @staticmethod async def create( @@ -108,6 +117,7 @@ class WalletStateManager: db_path: Path, constants: ConsensusConstants, server: ChiaServer, + root_path: Path, name: str = None, ): self = WalletStateManager() @@ -115,6 +125,7 @@ class WalletStateManager: self.config = config self.constants = constants self.server = server + self.root_path = root_path self.log = logging.getLogger(name if name else __name__) self.lock = asyncio.Lock() self.log.debug(f"Starting in db path: {db_path}") @@ -129,13 +140,16 @@ class WalletStateManager: self.trade_manager = await TradeManager.create(self, self.db_wrapper) self.user_settings = await UserSettings.create(self.basic_store) self.block_store = await WalletBlockStore.create(self.db_wrapper) + self.interested_store = await WalletInterestedStore.create(self.db_wrapper) + self.pool_store = await WalletPoolStore.create(self.db_wrapper) self.blockchain = await WalletBlockchain.create( self.block_store, self.coin_store, self.tx_store, + self.pool_store, self.constants, - self.coins_of_interest_received, + self.new_transaction_block_callback, self.reorg_rollback, self.lock, ) @@ -158,7 +172,6 @@ class WalletStateManager: wallet = None for wallet_info in await self.get_all_wallet_info_entries(): - # self.log.info(f"wallet_info {wallet_info}") if wallet_info.type == WalletType.STANDARD_WALLET: if wallet_info.id == 1: continue @@ -177,6 +190,12 @@ class WalletStateManager: self.main_wallet, wallet_info, ) + elif wallet_info.type == WalletType.POOLING_WALLET: + wallet = await PoolWallet.create_from_db( + self, + self.main_wallet, + wallet_info, + ) if wallet is not None: self.wallets[wallet_info.id] = wallet @@ -235,7 +254,7 @@ class WalletStateManager: pubkey = private.get_g1() return pubkey, private - async def create_more_puzzle_hashes(self, from_zero: bool = False): + async def create_more_puzzle_hashes(self, from_zero: bool = False, in_transaction=False): """ For all wallets in the user store, generates the first few puzzle hashes so that we can restore the wallet from only the private keys. @@ -271,6 +290,8 @@ class WalletStateManager: start_index = 0 for index in range(start_index, unused + to_generate): + if WalletType(target_wallet.type()) == WalletType.POOLING_WALLET: + continue if WalletType(target_wallet.type()) == WalletType.RATE_LIMITED: if target_wallet.rl_info.initialized is False: break @@ -314,9 +335,9 @@ class WalletStateManager: ) ) - await self.puzzle_store.add_derivation_paths(derivation_paths) + await self.puzzle_store.add_derivation_paths(derivation_paths, in_transaction) if unused > 0: - await self.puzzle_store.set_used_up_to(uint32(unused - 1)) + await self.puzzle_store.set_used_up_to(uint32(unused - 1), in_transaction) async def update_wallet_puzzle_hashes(self, wallet_id): derivation_paths: List[DerivationRecord] = [] @@ -345,7 +366,7 @@ class WalletStateManager: ) await self.puzzle_store.add_derivation_paths(derivation_paths) - async def get_unused_derivation_record(self, wallet_id: uint32) -> DerivationRecord: + async def get_unused_derivation_record(self, wallet_id: uint32, in_transaction=False) -> DerivationRecord: """ Creates a puzzle hash for the given wallet, and then makes more puzzle hashes for every wallet to ensure we always have more in the database. Never reusue the @@ -364,10 +385,10 @@ class WalletStateManager: assert record is not None # Set this key to used so we never use it again - await self.puzzle_store.set_used_up_to(record.index) + await self.puzzle_store.set_used_up_to(record.index, in_transaction=in_transaction) # Create more puzzle hashes / keys - await self.create_more_puzzle_hashes() + await self.create_more_puzzle_hashes(in_transaction=in_transaction) return record async def get_current_derivation_record_for_wallet(self, wallet_id: uint32) -> Optional[DerivationRecord]: @@ -390,12 +411,18 @@ class WalletStateManager: """ self.pending_tx_callback = callback - def set_coin_with_puzzlehash_created_callback(self, puzzlehash, callback: Callable): + def set_coin_with_puzzlehash_created_callback(self, puzzlehash: bytes32, callback: Callable): """ Callback to be called when new coin is seen with specified puzzlehash """ self.puzzle_hash_created_callbacks[puzzlehash] = callback + def set_new_peak_callback(self, wallet_id: int, callback: Callable): + """ + Callback to be called when blockchain adds new peak + """ + self.new_peak_callbacks[wallet_id] = callback + async def puzzle_hash_created(self, coin: Coin): callback = self.puzzle_hash_created_callbacks[coin.puzzle_hash] if callback is None: @@ -486,7 +513,6 @@ class WalletStateManager: amount: uint128 = uint128(0) for record in unspent_coin_records: amount = uint128(amount + record.coin.amount) - self.log.info(f"Confirmed balance amount is {amount}") return uint128(amount) async def get_unconfirmed_balance( @@ -536,13 +562,49 @@ class WalletStateManager: removals[coin.name()] = coin return removals - async def coins_of_interest_received(self, removals: List[Coin], additions: List[Coin], height: uint32): + async def new_transaction_block_callback( + self, + removals: List[Coin], + additions: List[Coin], + block: BlockRecord, + additional_coin_spends: List[CoinSolution], + ): + height: uint32 = block.height for coin in additions: await self.puzzle_hash_created(coin) - trade_additions, added = await self.coins_of_interest_added(additions, height) + trade_additions, added = await self.coins_of_interest_added(additions, block) trade_removals, removed = await self.coins_of_interest_removed(removals, height) if len(trade_additions) > 0 or len(trade_removals) > 0: await self.trade_manager.coins_of_interest_farmed(trade_removals, trade_additions, height) + + if len(additional_coin_spends) > 0: + created_pool_wallet_ids: List[int] = [] + for cs in additional_coin_spends: + if cs.coin.puzzle_hash == SINGLETON_LAUNCHER_HASH: + already_have = False + for wallet_id, wallet in self.wallets.items(): + if ( + wallet.type() == WalletType.POOLING_WALLET + and (await wallet.get_current_state()).launcher_id == cs.coin.name() + ): + self.log.warning("Already have, not recreating") + already_have = True + if not already_have: + try: + solution_to_extra_data(cs) + except Exception as e: + self.log.debug(f"Not a pool wallet launcher {e}") + continue + self.log.info("Found created launcher. Creating pool wallet") + pool_wallet = await PoolWallet.create( + self, self.main_wallet, cs.coin.name(), additional_coin_spends, height, True, "pool_wallet" + ) + created_pool_wallet_ids.append(pool_wallet.wallet_id) + + for wallet_id, wallet in self.wallets.items(): + if wallet.type() == WalletType.POOLING_WALLET: + await wallet.apply_state_transitions(additional_coin_spends, height) + added_notified = set() removed_notified = set() for coin_record in added: @@ -556,18 +618,17 @@ class WalletStateManager: removed_notified.add(coin_record.wallet_id) self.state_changed("coin_removed", coin_record.wallet_id) + self.tx_pending_changed() + async def coins_of_interest_added( - self, coins: List[Coin], height: uint32 + self, coins: List[Coin], block: BlockRecord ) -> Tuple[List[Coin], List[WalletCoinRecord]]: ( trade_removals, trade_additions, ) = await self.trade_manager.get_coins_of_interest() trade_adds: List[Coin] = [] - block: Optional[BlockRecord] = await self.blockchain.get_block_record_from_db( - self.blockchain.height_to_hash(height) - ) - assert block is not None + height = block.height pool_rewards = set() farmer_rewards = set() @@ -626,9 +687,32 @@ class WalletStateManager: if info is not None: wallet_id, wallet_type = info added_coin_record = await self.coin_added( - coin, is_coinbase, is_fee_reward, uint32(wallet_id), wallet_type, height, all_outgoing_tx[wallet_id] + coin, + is_coinbase, + is_fee_reward, + uint32(wallet_id), + wallet_type, + height, + all_outgoing_tx.get(wallet_id, []), ) added.append(added_coin_record) + else: + interested_wallet_id = await self.interested_store.get_interested_puzzle_hash_wallet_id( + puzzle_hash=coin.puzzle_hash + ) + if interested_wallet_id is not None: + wallet_type = self.wallets[uint32(interested_wallet_id)].type() + added_coin_record = await self.coin_added( + coin, + is_coinbase, + is_fee_reward, + uint32(interested_wallet_id), + wallet_type, + height, + all_outgoing_tx.get(interested_wallet_id, []), + ) + added.append(added_coin_record) + derivation_index = await self.puzzle_store.index_for_puzzle_hash(coin.puzzle_hash) if derivation_index is not None: await self.puzzle_store.set_used_up_to(derivation_index, True) @@ -639,7 +723,8 @@ class WalletStateManager: self, coins: List[Coin], height: uint32 ) -> Tuple[List[Coin], List[WalletCoinRecord]]: # This gets called when coins of our interest are spent on chain - self.log.info(f"Coins removed {coins} at height: {height}") + if len(coins) > 0: + self.log.info(f"Coins removed {coins} at height: {height}") ( trade_removals, trade_additions, @@ -655,15 +740,15 @@ class WalletStateManager: trade_coin_removed.append(coin) if record is None: self.log.info(f"Record for removed coin {coin.name()} is None. (ephemeral)") - continue - await self.coin_store.set_spent(coin.name(), height) + else: + await self.coin_store.set_spent(coin.name(), height) for unconfirmed_record in all_unconfirmed: for rem_coin in unconfirmed_record.removals: if rem_coin.name() == coin.name(): self.log.info(f"Setting tx_id: {unconfirmed_record.name} to confirmed") await self.tx_store.set_confirmed(unconfirmed_record.name, height) - - removed.append(record) + if record is not None: + removed.append(record) return trade_coin_removed, removed @@ -748,11 +833,7 @@ class WalletStateManager: if wallet_type == WalletType.COLOURED_COIN or wallet_type == WalletType.DISTRIBUTED_ID: wallet = self.wallets[wallet_id] - header_hash: bytes32 = self.blockchain.height_to_hash(height) - block: Optional[HeaderBlockRecord] = await self.block_store.get_header_block_record(header_hash) - assert block is not None - assert block.removals is not None - await wallet.coin_added(coin, header_hash, block.removals, height) + await wallet.coin_added(coin, height) return coin_record @@ -790,13 +871,6 @@ class WalletStateManager: if tx is not None: self.state_changed("tx_update", tx.wallet_id, {"transaction": tx}) - async def get_send_queue(self) -> List[TransactionRecord]: - """ - Wallet Node uses this to retry sending transactions - """ - records = await self.tx_store.get_not_sent() - return records - async def get_all_transactions(self, wallet_id: int) -> List[TransactionRecord]: """ Retrieves all confirmed and pending transactions @@ -888,20 +962,16 @@ class WalletStateManager: if tx_filter.Match(bytearray(puzzle_hash)): additions_of_interest.append(puzzle_hash) + for coin_id in await self.interested_store.get_interested_coin_ids(): + if tx_filter.Match(bytearray(coin_id)): + removals_of_interest.append(coin_id) + + for puzzle_hash, _ in await self.interested_store.get_interested_puzzle_hashes(): + if tx_filter.Match(bytearray(puzzle_hash)): + additions_of_interest.append(puzzle_hash) + return additions_of_interest, removals_of_interest - async def get_relevant_additions(self, additions: List[Coin]) -> List[Coin]: - """Returns the list of coins that are relevant to us.(We can spend them)""" - - result: List[Coin] = [] - my_puzzle_hashes: Set[bytes32] = self.puzzle_store.all_puzzle_hashes - - for coin in additions: - if coin.puzzle_hash in my_puzzle_hashes: - result.append(coin) - - return result - async def is_addition_relevant(self, addition: Coin): """ Check whether we care about a new addition (puzzle_hash). Returns true if we @@ -918,19 +988,6 @@ class WalletStateManager: wallet = self.wallets[wallet_id] return wallet - async def get_relevant_removals(self, removals: List[Coin]) -> List[Coin]: - """Returns a list of our unspent coins that are in the passed list.""" - - result: List[Coin] = [] - wallet_coin_records = await self.coin_store.get_unspent_coins_at_height() - my_coins: Dict[bytes32, Coin] = {r.coin.name(): r.coin for r in list(wallet_coin_records)} - - for coin in removals: - if coin.name() in my_coins: - result.append(coin) - - return result - async def reorg_rollback(self, height: int): """ Rolls back and updates the coin_store and transaction store. It's possible this height @@ -941,17 +998,7 @@ class WalletStateManager: reorged: List[TransactionRecord] = await self.tx_store.get_transaction_above(height) await self.tx_store.rollback_to_block(height) - await self.retry_sending_after_reorg(reorged) - - async def retry_sending_after_reorg(self, records: List[TransactionRecord]): - """ - Retries sending spend_bundle to the Full_Node, after confirmed tx - get's excluded from chain because of the reorg. - """ - if len(records) == 0: - return None - - for record in records: + for record in reorged: if record.type in [ TransactionType.OUTGOING_TX, TransactionType.OUTGOING_TRADE, @@ -959,7 +1006,17 @@ class WalletStateManager: ]: await self.tx_store.tx_reorged(record) - self.tx_pending_changed() + # Removes wallets that were created from a blockchain transaction which got reorged. + remove_ids = [] + for wallet_id, wallet in self.wallets.items(): + if wallet.type() == WalletType.POOLING_WALLET.value: + remove: bool = await wallet.rewind(height) + if remove: + remove_ids.append(wallet_id) + for wallet_id in remove_ids: + await self.user_store.delete_wallet(wallet_id, in_transaction=True) + self.wallets.pop(wallet_id) + self.new_peak_callbacks.pop(wallet_id) async def close_all_stores(self) -> None: if self.blockchain is not None: @@ -1051,14 +1108,15 @@ class WalletStateManager: return wallet return None - async def add_new_wallet(self, wallet: Any, wallet_id: int): + async def add_new_wallet(self, wallet: Any, wallet_id: int, create_puzzle_hashes=True): self.wallets[uint32(wallet_id)] = wallet - await self.create_more_puzzle_hashes() + if create_puzzle_hashes: + await self.create_more_puzzle_hashes() # search through the blockrecords and return the most recent coin to use a given puzzlehash async def search_blockrecords_for_puzzlehash(self, puzzlehash: bytes32): header_hash_of_interest = None - heighest_block_height = 0 + highest_block_height = 0 peak: Optional[BlockRecord] = self.blockchain.get_peak() if peak is None: return None, None @@ -1067,16 +1125,16 @@ class WalletStateManager: ) while peak_block is not None: tx_filter = PyBIP158([b for b in peak_block.header.transactions_filter]) - if tx_filter.Match(bytearray(puzzlehash)) and peak_block.height > heighest_block_height: + if tx_filter.Match(bytearray(puzzlehash)) and peak_block.height > highest_block_height: header_hash_of_interest = peak_block.header_hash - heighest_block_height = peak_block.height + highest_block_height = peak_block.height break else: peak_block = await self.blockchain.block_store.get_header_block_record( peak_block.header.prev_header_hash ) - return heighest_block_height, header_hash_of_interest + return highest_block_height, header_hash_of_interest async def get_spendable_coins_for_wallet(self, wallet_id: int, records=None) -> Set[WalletCoinRecord]: if self.peak is None: @@ -1151,3 +1209,20 @@ class WalletStateManager: if callback_str is not None: callback = getattr(wallet, callback_str) await callback(unwrapped, action.id) + + def get_peak(self) -> Optional[BlockRecord]: + return self.blockchain.get_peak() + + async def get_next_interesting_coin_ids(self, spend: CoinSolution, in_transaction: bool) -> List[bytes32]: + pool_wallet_interested: List[bytes32] = PoolWallet.get_next_interesting_coin_ids(spend) + for coin_id in pool_wallet_interested: + await self.interested_store.add_interested_coin_id(coin_id, in_transaction) + return pool_wallet_interested + + async def new_peak(self): + peak: Optional[BlockRecord] = self.get_peak() + if peak is None: + return + + for wallet_id, callback in self.new_peak_callbacks.items(): + await callback(peak) diff --git a/chia/wallet/wallet_transaction_store.py b/chia/wallet/wallet_transaction_store.py index 5db786345b88..a07d97150cbf 100644 --- a/chia/wallet/wallet_transaction_store.py +++ b/chia/wallet/wallet_transaction_store.py @@ -91,9 +91,9 @@ class WalletTransactionStore: for record in all_records: self.tx_record_cache[record.name] = record if record.wallet_id not in self.unconfirmed_for_wallet: - self.unconfirmed_for_wallet[record.name] = {} + self.unconfirmed_for_wallet[record.wallet_id] = {} if not record.confirmed: - self.unconfirmed_for_wallet[record.name] = record + self.unconfirmed_for_wallet[record.wallet_id][record.name] = record async def _clear_database(self): cursor = await self.db_connection.execute("DELETE FROM transaction_record") @@ -290,7 +290,7 @@ class WalletTransactionStore: return records - async def get_farming_rewards(self): + async def get_farming_rewards(self) -> List[TransactionRecord]: """ Returns the list of all farming rewards. """ @@ -440,3 +440,9 @@ class WalletTransactionStore: c1 = await self.db_connection.execute("DELETE FROM transaction_record WHERE confirmed_at_height>?", (height,)) await c1.close() + + async def delete_unconfirmed_transactions(self, wallet_id: int): + cursor = await self.db_connection.execute( + "DELETE FROM transaction_record WHERE confirmed=0 AND wallet_id=?", (wallet_id,) + ) + await cursor.close() diff --git a/chia/wallet/wallet_user_store.py b/chia/wallet/wallet_user_store.py index f09396450d07..9bbc422ed5e9 100644 --- a/chia/wallet/wallet_user_store.py +++ b/chia/wallet/wallet_user_store.py @@ -56,22 +56,34 @@ class WalletUserStore: await self.db_connection.commit() async def create_wallet( - self, name: str, wallet_type: int, data: str, id: Optional[int] = None + self, name: str, wallet_type: int, data: str, id: Optional[int] = None, in_transaction=False ) -> Optional[WalletInfo]: - async with self.db_wrapper.lock: + + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: cursor = await self.db_connection.execute( "INSERT INTO users_wallets VALUES(?, ?, ?, ?)", (id, name, wallet_type, data), ) await cursor.close() - await self.db_connection.commit() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() + return await self.get_last_wallet() - async def delete_wallet(self, id: int): - async with self.db_wrapper.lock: + async def delete_wallet(self, id: int, in_transaction: bool): + if not in_transaction: + await self.db_wrapper.lock.acquire() + try: cursor = await self.db_connection.execute(f"DELETE FROM users_wallets where id={id}") await cursor.close() - await self.db_connection.commit() + finally: + if not in_transaction: + await self.db_connection.commit() + self.db_wrapper.lock.release() async def update_wallet(self, wallet_info: WalletInfo, in_transaction): if not in_transaction: diff --git a/install-gui.sh b/install-gui.sh index fd848b0b5b25..ec70f2d5aae8 100755 --- a/install-gui.sh +++ b/install-gui.sh @@ -1,5 +1,7 @@ #!/bin/bash set -e +export NODE_OPTIONS="--max-old-space-size=3000" + if [ -z "$VIRTUAL_ENV" ]; then echo "This requires the chia python virtual environment." diff --git a/setup.py b/setup.py index b41ed42a8e3d..3aa12b76ff7f 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ kwargs = dict( "build_scripts", "chia", "chia.cmds", + "chia.clvm", "chia.consensus", "chia.daemon", "chia.full_node", @@ -68,6 +69,7 @@ kwargs = dict( "chia.harvester", "chia.introducer", "chia.plotting", + "chia.pools", "chia.protocols", "chia.rpc", "chia.server", diff --git a/tests/block_tools.py b/tests/block_tools.py index 723a76085379..1ef43dfee891 100644 --- a/tests/block_tools.py +++ b/tests/block_tools.py @@ -9,7 +9,7 @@ import time from argparse import Namespace from dataclasses import replace from pathlib import Path -from typing import Callable, Dict, List, Optional, Tuple +from typing import Callable, Dict, List, Optional, Tuple, Any from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey from chiabip158 import PyBIP158 @@ -153,13 +153,12 @@ class BlockTools: self.all_sks: List[PrivateKey] = [sk for sk, _ in self.keychain.get_all_private_keys()] self.pool_pubkeys: List[G1Element] = [master_sk_to_pool_sk(sk).get_g1() for sk in self.all_sks] - farmer_pubkeys: List[G1Element] = [master_sk_to_farmer_sk(sk).get_g1() for sk in self.all_sks] - if len(self.pool_pubkeys) == 0 or len(farmer_pubkeys) == 0: + self.farmer_pubkeys: List[G1Element] = [master_sk_to_farmer_sk(sk).get_g1() for sk in self.all_sks] + if len(self.pool_pubkeys) == 0 or len(self.farmer_pubkeys) == 0: raise RuntimeError("Keys not generated. Run `chia generate keys`") - _, loaded_plots, _, _ = load_plots({}, {}, farmer_pubkeys, self.pool_pubkeys, None, False, root_path) - self.plots: Dict[Path, PlotInfo] = loaded_plots - self.local_sk_cache: Dict[bytes32, PrivateKey] = {} + self.load_plots() + self.local_sk_cache: Dict[bytes32, Tuple[PrivateKey, Any]] = {} self._config = load_config(self.root_path, "config.yaml") self._config["logging"]["log_stdout"] = True self._config["selected_network"] = "testnet0" @@ -179,10 +178,14 @@ class BlockTools: self.constants = updated_constants save_config(self.root_path, "config.yaml", self._config) + def load_plots(self): + _, loaded_plots, _, _ = load_plots({}, {}, self.farmer_pubkeys, self.pool_pubkeys, None, False, self.root_path) + self.plots: Dict[Path, PlotInfo] = loaded_plots + def init_plots(self, root_path: Path): plot_dir = get_plot_dir() mkdir(plot_dir) - temp_dir = plot_dir / "tmp" + temp_dir = get_plot_tmp_dir() mkdir(temp_dir) num_pool_public_key_plots = 15 num_pool_address_plots = 5 @@ -245,16 +248,26 @@ class BlockTools: if plot_pk == plot_info.plot_public_key: # Look up local_sk from plot to save locked memory if plot_info.prover.get_id() in self.local_sk_cache: - local_master_sk = self.local_sk_cache[plot_info.prover.get_id()] + local_master_sk, pool_pk_or_ph = self.local_sk_cache[plot_info.prover.get_id()] else: - _, _, local_master_sk = parse_plot_info(plot_info.prover.get_memo()) - self.local_sk_cache[plot_info.prover.get_id()] = local_master_sk + pool_pk_or_ph, _, local_master_sk = parse_plot_info(plot_info.prover.get_memo()) + self.local_sk_cache[plot_info.prover.get_id()] = (local_master_sk, pool_pk_or_ph) + if isinstance(pool_pk_or_ph, G1Element): + include_taproot = False + else: + assert isinstance(pool_pk_or_ph, bytes32) + include_taproot = True local_sk = master_sk_to_local_sk(local_master_sk) - agg_pk = ProofOfSpace.generate_plot_public_key(local_sk.get_g1(), farmer_sk.get_g1()) + agg_pk = ProofOfSpace.generate_plot_public_key(local_sk.get_g1(), farmer_sk.get_g1(), include_taproot) assert agg_pk == plot_pk harv_share = AugSchemeMPL.sign(local_sk, m, agg_pk) farm_share = AugSchemeMPL.sign(farmer_sk, m, agg_pk) - return AugSchemeMPL.aggregate([harv_share, farm_share]) + if include_taproot: + taproot_sk: PrivateKey = ProofOfSpace.generate_taproot_sk(local_sk.get_g1(), farmer_sk.get_g1()) + taproot_share: G2Element = AugSchemeMPL.sign(taproot_sk, m, agg_pk) + else: + taproot_share = G2Element() + return AugSchemeMPL.aggregate([harv_share, farm_share, taproot_share]) raise ValueError(f"Do not have key {plot_pk}") @@ -294,6 +307,7 @@ class BlockTools: current_time: bool = False, previous_generator: CompressorArg = None, genesis_timestamp: Optional[uint64] = None, + force_plot_id: Optional[bytes32] = None, ) -> List[FullBlock]: assert num_blocks > 0 if block_list_input is not None: @@ -309,6 +323,8 @@ class BlockTools: farmer_reward_puzzle_hash = self.farmer_ph if len(block_list) == 0: + if force_plot_id is not None: + raise ValueError("Cannot specify plot_id for genesis block") initial_block_list_len = 0 genesis = self.create_genesis_block( constants, @@ -406,6 +422,7 @@ class BlockTools: seed, difficulty, sub_slot_iters, + force_plot_id=force_plot_id, ) for required_iters, proof_of_space in sorted(qualified_proofs, key=lambda t: t[0]): @@ -683,6 +700,7 @@ class BlockTools: seed, difficulty, sub_slot_iters, + force_plot_id=force_plot_id, ) for required_iters, proof_of_space in sorted(qualified_proofs, key=lambda t: t[0]): if blocks_added_this_sub_slot == constants.MAX_SUB_SLOT_BLOCKS: @@ -970,6 +988,7 @@ class BlockTools: seed: bytes, difficulty: uint64, sub_slot_iters: uint64, + force_plot_id: Optional[bytes32] = None, ) -> List[Tuple[uint64, ProofOfSpace]]: found_proofs: List[Tuple[uint64, ProofOfSpace]] = [] plots: List[PlotInfo] = [ @@ -977,7 +996,9 @@ class BlockTools: ] random.seed(seed) for plot_info in plots: - plot_id = plot_info.prover.get_id() + plot_id: bytes32 = plot_info.prover.get_id() + if force_plot_id is not None and plot_id != force_plot_id: + continue if ProofOfSpace.passes_plot_filter(constants, plot_id, challenge_hash, signage_point): new_challenge: bytes32 = ProofOfSpace.calculate_pos_challenge(plot_id, challenge_hash, signage_point) qualities = plot_info.prover.get_qualities_for_challenge(new_challenge) @@ -1001,9 +1022,14 @@ class BlockTools: local_master_sk, ) = parse_plot_info(plot_info.prover.get_memo()) local_sk = master_sk_to_local_sk(local_master_sk) + + if isinstance(pool_public_key_or_puzzle_hash, G1Element): + include_taproot = False + else: + assert isinstance(pool_public_key_or_puzzle_hash, bytes32) + include_taproot = True plot_pk = ProofOfSpace.generate_plot_public_key( - local_sk.get_g1(), - farmer_public_key, + local_sk.get_g1(), farmer_public_key, include_taproot ) proof_of_space: ProofOfSpace = ProofOfSpace( new_challenge, @@ -1198,6 +1224,10 @@ def get_plot_dir() -> Path: return cache_path +def get_plot_tmp_dir(): + return get_plot_dir() / "tmp" + + def load_block_list( block_list: List[FullBlock], constants: ConsensusConstants ) -> Tuple[Dict[uint32, bytes32], uint64, Dict[uint32, BlockRecord]]: diff --git a/tests/clvm/coin_store.py b/tests/clvm/coin_store.py index af9280bad34d..aa6a92acb318 100644 --- a/tests/clvm/coin_store.py +++ b/tests/clvm/coin_store.py @@ -1,8 +1,8 @@ from collections import defaultdict from dataclasses import dataclass, replace -from typing import Dict, Iterator, Set +from typing import Dict, Iterator, Optional, Set -from chia.full_node.mempool_check_conditions import mempool_check_conditions_dict +from chia.full_node.mempool_check_conditions import mempool_check_conditions_dict # noqa from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.coin_record import CoinRecord @@ -15,6 +15,9 @@ from chia.util.condition_tools import ( from chia.util.ints import uint32, uint64 +MAX_COST = 11000000000 + + class BadSpendBundleError(Exception): pass @@ -26,12 +29,28 @@ class CoinTimestamp: class CoinStore: - def __init__(self): + def __init__(self, reward_mask: int = 0): self._db: Dict[bytes32, CoinRecord] = dict() - self._ph_index = defaultdict(list) + self._ph_index: Dict = defaultdict(list) + self._reward_mask = reward_mask - def farm_coin(self, puzzle_hash: bytes32, birthday: CoinTimestamp, amount: int = 1024) -> Coin: - parent = birthday.height.to_bytes(32, "big") + def farm_coin( + self, + puzzle_hash: bytes32, + birthday: CoinTimestamp, + amount: int = 1024, + prefix=bytes32.fromhex("ccd5bb71183532bff220ba46c268991a00000000000000000000000000000000"), # noqa + ) -> Coin: + parent = bytes32( + [ + a | b + for a, b in zip( + prefix, + birthday.height.to_bytes(32, "big"), + ) + ], + ) + # parent = birthday.height.to_bytes(32, "big") coin = Coin(parent, puzzle_hash, uint64(amount)) self._add_coin_entry(coin, birthday) return coin @@ -49,6 +68,7 @@ class CoinStore: conditions_dicts = [] for coin_solution in spend_bundle.coin_solutions: + assert isinstance(coin_solution.coin, Coin) err, conditions_dict, cost = conditions_dict_for_solution( coin_solution.puzzle_reveal, coin_solution.solution, max_cost ) @@ -56,16 +76,36 @@ class CoinStore: raise BadSpendBundleError(f"clvm validation failure {err}") conditions_dicts.append(conditions_dict) coin_announcements.update( - coin_announcement_names_for_conditions_dict(conditions_dict, coin_solution.coin.name()) + coin_announcement_names_for_conditions_dict( + conditions_dict, + coin_solution.coin, + ) ) puzzle_announcements.update( - puzzle_announcement_names_for_conditions_dict(conditions_dict, coin_solution.coin.puzzle_hash) + puzzle_announcement_names_for_conditions_dict( + conditions_dict, + coin_solution.coin, + ) ) - for coin_solution, conditions_dict in zip(spend_bundle.coin_solutions, conditions_dicts): + ephemeral_db = dict(self._db) + for coin in spend_bundle.additions(): + name = coin.name() + ephemeral_db[name] = CoinRecord( + coin, + uint32(now.height), + uint32(0), + False, + False, + uint64(now.seconds), + ) + + for coin_solution, conditions_dict in zip(spend_bundle.coin_solutions, conditions_dicts): # noqa prev_transaction_block_height = now.height timestamp = now.seconds - coin_record = self._db[coin_solution.coin.name()] + coin_record = ephemeral_db.get(coin_solution.coin.name()) + if coin_record is None: + raise BadSpendBundleError(f"coin not found for id 0x{coin_solution.coin.name().hex()}") # noqa err = mempool_check_conditions_dict( coin_record, coin_announcements, @@ -79,16 +119,24 @@ class CoinStore: return 0 - def update_coin_store_for_spend_bundle(self, spend_bundle: SpendBundle, now: CoinTimestamp, max_cost: int): + def update_coin_store_for_spend_bundle( + self, + spend_bundle: SpendBundle, + now: CoinTimestamp, + max_cost: int, + ): err = self.validate_spend_bundle(spend_bundle, now, max_cost) if err != 0: raise BadSpendBundleError(f"validation failure {err}") - for spent_coin in spend_bundle.removals(): + additions = spend_bundle.additions() + removals = spend_bundle.removals() + for new_coin in additions: + self._add_coin_entry(new_coin, now) + for spent_coin in removals: coin_name = spent_coin.name() coin_record = self._db[coin_name] self._db[coin_name] = replace(coin_record, spent_block_index=now.height, spent=True) - for new_coin in spend_bundle.additions(): - self._add_coin_entry(new_coin, now) + return additions, spend_bundle.coin_solutions def coins_for_puzzle_hash(self, puzzle_hash: bytes32) -> Iterator[Coin]: for coin_name in self._ph_index[puzzle_hash]: @@ -100,8 +148,23 @@ class CoinStore: for coin_entry in self._db.values(): yield coin_entry.coin + def all_unspent_coins(self) -> Iterator[Coin]: + for coin_entry in self._db.values(): + if not coin_entry.spent: + yield coin_entry.coin + def _add_coin_entry(self, coin: Coin, birthday: CoinTimestamp) -> None: name = coin.name() - assert name not in self._db - self._db[name] = CoinRecord(coin, uint32(birthday.height), uint32(0), False, False, uint64(birthday.seconds)) + # assert name not in self._db + self._db[name] = CoinRecord( + coin, + uint32(birthday.height), + uint32(0), + False, + False, + uint64(birthday.seconds), + ) self._ph_index[coin.puzzle_hash].append(name) + + def coin_record(self, coin_id: bytes32) -> Optional[CoinRecord]: + return self._db.get(coin_id) diff --git a/tests/clvm/test_clvm_compilation.py b/tests/clvm/test_clvm_compilation.py index b9d8343df492..32964473a705 100644 --- a/tests/clvm/test_clvm_compilation.py +++ b/tests/clvm/test_clvm_compilation.py @@ -32,6 +32,11 @@ wallet_program_files = set( "chia/wallet/puzzles/block_program_zero.clvm", "chia/wallet/puzzles/test_generator_deserialize.clvm", "chia/wallet/puzzles/test_multiple_generator_input_arguments.clvm", + "chia/wallet/puzzles/p2_singleton.clvm", + "chia/wallet/puzzles/pool_waitingroom_innerpuz.clvm", + "chia/wallet/puzzles/pool_member_innerpuz.clvm", + "chia/wallet/puzzles/singleton_launcher.clvm", + "chia/wallet/puzzles/p2_singleton_or_delayed_puzhash.clvm", ] ) diff --git a/tests/clvm/test_singletons.py b/tests/clvm/test_singletons.py new file mode 100644 index 000000000000..cfa0c5051570 --- /dev/null +++ b/tests/clvm/test_singletons.py @@ -0,0 +1,519 @@ +import copy + +from typing import List, Tuple, Optional +from unittest import TestCase + +from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.coin import Coin +from chia.types.coin_solution import CoinSolution +from chia.types.spend_bundle import SpendBundle +from chia.util.condition_tools import ConditionOpcode +from chia.util.ints import uint64 +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.wallet.lineage_proof import LineageProof +from chia.wallet.puzzles import ( + p2_conditions, + p2_delegated_puzzle_or_hidden_puzzle, + singleton_top_layer, +) +from tests.util.key_tool import KeyTool +from tests.clvm.test_puzzles import ( + public_key_for_index, + secret_exponent_for_index, +) + +from .coin_store import CoinStore, CoinTimestamp, BadSpendBundleError + +""" +This test suite aims to test: + - chia.wallet.puzzles.singleton_top_layer.py + - chia.wallet.puzzles.singleton_top_layer.clvm + - chia.wallet.puzzles.p2_singleton.clvm + - chia.wallet.puzzles.p2_singleton_or_delayed_puzhash.clvm +""" + +T1 = CoinTimestamp(1, 10000000) + + +# Helper function +def sign_delegated_puz(del_puz: Program, coin: Coin) -> G2Element: + synthetic_secret_key: PrivateKey = p2_delegated_puzzle_or_hidden_puzzle.calculate_synthetic_secret_key( # noqa + PrivateKey.from_bytes( + secret_exponent_for_index(1).to_bytes(32, "big"), + ), + p2_delegated_puzzle_or_hidden_puzzle.DEFAULT_HIDDEN_PUZZLE_HASH, + ) + return AugSchemeMPL.sign( + synthetic_secret_key, + (del_puz.get_tree_hash() + coin.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), # noqa + ) + + +# Helper function +def make_and_spend_bundle( + db: CoinStore, + coin: Coin, + delegated_puzzle: Program, + coinsols: List[CoinSolution], + exception: Optional[Exception] = None, + ex_msg: str = "", + fail_msg: str = "", +): + + signature: G2Element = sign_delegated_puz(delegated_puzzle, coin) + spend_bundle = SpendBundle( + coinsols, + signature, + ) + + try: + db.update_coin_store_for_spend_bundle( + spend_bundle, + T1, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + if exception is not None: + raise AssertionError(fail_msg) + except Exception as e: + if exception is not None: + assert type(e) is exception + assert str(e) == ex_msg + else: + raise e + + +class TestSingleton(TestCase): + def test_singleton_top_layer(self): + # START TESTS + # Generate starting info + key_lookup = KeyTool() + pk: G1Element = public_key_for_index(1, key_lookup) + starting_puzzle: Program = p2_delegated_puzzle_or_hidden_puzzle.puzzle_for_pk(pk) # noqa + adapted_puzzle: Program = singleton_top_layer.adapt_inner_to_singleton(starting_puzzle) # noqa + adapted_puzzle_hash: bytes32 = adapted_puzzle.get_tree_hash() + + # Get our starting standard coin created + START_AMOUNT: uint64 = 1023 + coin_db = CoinStore() + coin_db.farm_coin(starting_puzzle.get_tree_hash(), T1, START_AMOUNT) + starting_coin: Coin = next(coin_db.all_unspent_coins()) + comment: List[Tuple[str, str]] = [("hello", "world")] + + # LAUNCHING + # Try to create an even singleton (driver test) + try: + conditions, launcher_coinsol = singleton_top_layer.launch_conditions_and_coinsol( # noqa + starting_coin, adapted_puzzle, comment, (START_AMOUNT - 1) + ) + raise AssertionError("This should fail due to an even amount") + except ValueError as msg: + assert str(msg) == "Coin amount cannot be even. Subtract one mojo." + conditions, launcher_coinsol = singleton_top_layer.launch_conditions_and_coinsol( # noqa + starting_coin, adapted_puzzle, comment, START_AMOUNT + ) + + # Creating solution for standard transaction + delegated_puzzle: Program = p2_conditions.puzzle_for_conditions(conditions) # noqa + full_solution: Program = p2_delegated_puzzle_or_hidden_puzzle.solution_for_conditions(conditions) # noqa + + starting_coinsol = CoinSolution( + starting_coin, + starting_puzzle, + full_solution, + ) + + make_and_spend_bundle( + coin_db, + starting_coin, + delegated_puzzle, + [starting_coinsol, launcher_coinsol], + ) + + # EVE + singleton_eve: Coin = next(coin_db.all_unspent_coins()) + launcher_coin: Coin = singleton_top_layer.generate_launcher_coin( + starting_coin, + START_AMOUNT, + ) + launcher_id: bytes32 = launcher_coin.name() + # This delegated puzzle just recreates the coin exactly + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ + ConditionOpcode.CREATE_COIN, + adapted_puzzle_hash, + singleton_eve.amount, + ] + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + # Generate the lineage proof we will need from the launcher coin + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(launcher_coinsol) # noqa + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, + singleton_eve.amount, + inner_solution, + ) + + singleton_eve_coinsol = CoinSolution( + singleton_eve, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + coin_db, + singleton_eve, + delegated_puzzle, + [singleton_eve_coinsol], + ) + + # POST-EVE + singleton: Coin = next(coin_db.all_unspent_coins()) + # Same delegated_puzzle / inner_solution. We're just recreating ourself + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(singleton_eve_coinsol) # noqa + # Same puzzle_reveal too + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, + singleton.amount, + inner_solution, + ) + + singleton_coinsol = CoinSolution( + singleton, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + coin_db, + singleton, + delegated_puzzle, + [singleton_coinsol], + ) + + # CLAIM A P2_SINGLETON + singleton_child: Coin = next(coin_db.all_unspent_coins()) + p2_singleton_puz: Program = singleton_top_layer.pay_to_singleton_puzzle(launcher_id) + p2_singleton_ph: bytes32 = p2_singleton_puz.get_tree_hash() + ARBITRARY_AMOUNT: uint64 = 1379 + coin_db.farm_coin(p2_singleton_ph, T1, ARBITRARY_AMOUNT) + p2_singleton_coin: Coin = list( + filter( + lambda e: e.amount == ARBITRARY_AMOUNT, + list(coin_db.all_unspent_coins()), + ) + )[0] + assertion, announcement, claim_coinsol = singleton_top_layer.claim_p2_singleton( + p2_singleton_coin, + adapted_puzzle_hash, + launcher_id, + ) + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, singleton_eve.amount], + assertion, + announcement, + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(singleton_coinsol) + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, + singleton_eve.amount, + inner_solution, + ) + singleton_claim_coinsol = CoinSolution( + singleton_child, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle(coin_db, singleton_child, delegated_puzzle, [singleton_claim_coinsol, claim_coinsol]) + + # CLAIM A P2_SINGLETON_OR_DELAYED + singleton_child: Coin = next(coin_db.all_unspent_coins()) + DELAY_TIME: uint64 = 1 + DELAY_PH: bytes32 = adapted_puzzle_hash + p2_singleton_puz: Program = singleton_top_layer.pay_to_singleton_or_delay_puzzle( + launcher_id, + DELAY_TIME, + DELAY_PH, + ) + p2_singleton_ph: bytes32 = p2_singleton_puz.get_tree_hash() + ARBITRARY_AMOUNT: uint64 = 1379 + coin_db.farm_coin(p2_singleton_ph, T1, ARBITRARY_AMOUNT) + p2_singleton_coin: Coin = list( + filter( + lambda e: e.amount == ARBITRARY_AMOUNT, + list(coin_db.all_unspent_coins()), + ) + )[0] + assertion, announcement, claim_coinsol = singleton_top_layer.claim_p2_singleton( + p2_singleton_coin, + adapted_puzzle_hash, + launcher_id, + ) + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, singleton_eve.amount], + assertion, + announcement, + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(singleton_coinsol) + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, + singleton_eve.amount, + inner_solution, + ) + delay_claim_coinsol = CoinSolution( + singleton_child, + puzzle_reveal, + full_solution, + ) + + # Fork it so we can try the other spend types + fork_coin_db: CoinStore = copy.deepcopy(coin_db) + fork_coin_db_2: CoinStore = copy.deepcopy(coin_db) + make_and_spend_bundle(coin_db, singleton_child, delegated_puzzle, [delay_claim_coinsol, claim_coinsol]) + + # TRY TO SPEND AWAY TOO SOON (Negative Test) + to_delay_ph_coinsol = singleton_top_layer.spend_to_delayed_puzzle( + p2_singleton_coin, + ARBITRARY_AMOUNT, + launcher_id, + DELAY_TIME, + DELAY_PH, + ) + try: + fork_coin_db.update_coin_store_for_spend_bundle( + SpendBundle([to_delay_ph_coinsol], G2Element()), + T1, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + except BadSpendBundleError as e: + assert str(e) == "condition validation failure Err.ASSERT_SECONDS_RELATIVE_FAILED" + + # SPEND TO DELAYED PUZZLE HASH + fork_coin_db_2.update_coin_store_for_spend_bundle( + SpendBundle([to_delay_ph_coinsol], G2Element()), + CoinTimestamp(100, 10000005), + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + + # CREATE MULTIPLE ODD CHILDREN (Negative Test) + singleton_child: Coin = next(coin_db.all_unspent_coins()) + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, 3], + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, 7], + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(singleton_coinsol) # noqa + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, singleton_child.amount, inner_solution + ) + + multi_odd_coinsol = CoinSolution( + singleton_child, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + coin_db, + singleton_child, + delegated_puzzle, + [multi_odd_coinsol], + exception=BadSpendBundleError, + ex_msg="clvm validation failure Err.SEXP_ERROR", + fail_msg="Too many odd children were allowed", + ) + + # CREATE NO ODD CHILDREN (Negative Test) + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, 4], + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, 10], + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(singleton_coinsol) # noqa + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, singleton_child.amount, inner_solution + ) + + no_odd_coinsol = CoinSolution( + singleton_child, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + coin_db, + singleton_child, + delegated_puzzle, + [no_odd_coinsol], + exception=BadSpendBundleError, + ex_msg="clvm validation failure Err.SEXP_ERROR", + fail_msg="Need at least one odd child", + ) + + # ATTEMPT TO CREATE AN EVEN SINGLETON (Negative test) + fork_coin_db: CoinStore = copy.deepcopy(coin_db) + + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ + ConditionOpcode.CREATE_COIN, + singleton_child.puzzle_hash, + 2, + ], + [ConditionOpcode.CREATE_COIN, adapted_puzzle_hash, 1], + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(delay_claim_coinsol) + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, singleton_child.amount, inner_solution + ) + + singleton_even_coinsol = CoinSolution( + singleton_child, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + fork_coin_db, + singleton_child, + delegated_puzzle, + [singleton_even_coinsol], + ) + + # Now try a perfectly innocent spend + evil_coin: Coin = next(fork_coin_db.all_unspent_coins()) + delegated_puzzle: Program = Program.to( + ( + 1, + [ + [ + ConditionOpcode.CREATE_COIN, + adapted_puzzle_hash, + 1, + ], + ], + ) + ) + inner_solution: Program = Program.to([[], delegated_puzzle, []]) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(singleton_even_coinsol) # noqa + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, + 1, + inner_solution, + ) + + evil_coinsol = CoinSolution( + evil_coin, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + fork_coin_db, + evil_coin, + delegated_puzzle, + [evil_coinsol], + exception=BadSpendBundleError, + ex_msg="condition validation failure Err.ASSERT_MY_COIN_ID_FAILED", + fail_msg="This coin is even!", + ) + + # MELTING + # Remember, we're still spending singleton_child + conditions = [ + singleton_top_layer.MELT_CONDITION, + [ + ConditionOpcode.CREATE_COIN, + adapted_puzzle_hash, + (singleton_child.amount - 1), + ], + ] + delegated_puzzle: Program = p2_conditions.puzzle_for_conditions(conditions) + inner_solution: Program = p2_delegated_puzzle_or_hidden_puzzle.solution_for_conditions(conditions) + lineage_proof: LineageProof = singleton_top_layer.lineage_proof_for_coinsol(delay_claim_coinsol) + puzzle_reveal: Program = singleton_top_layer.puzzle_for_singleton( + launcher_id, + adapted_puzzle, + ) + full_solution: Program = singleton_top_layer.solution_for_singleton( + lineage_proof, singleton_child.amount, inner_solution + ) + + melt_coinsol = CoinSolution( + singleton_child, + puzzle_reveal, + full_solution, + ) + + make_and_spend_bundle( + coin_db, + singleton_child, + delegated_puzzle, + [melt_coinsol], + ) + + melted_coin = next(coin_db.all_unspent_coins()) + assert melted_coin.puzzle_hash == adapted_puzzle_hash diff --git a/tests/core/full_node/full_sync/test_full_sync.py b/tests/core/full_node/full_sync/test_full_sync.py index 3671162aac02..3cb8e7aaa6f6 100644 --- a/tests/core/full_node/full_sync/test_full_sync.py +++ b/tests/core/full_node/full_sync/test_full_sync.py @@ -14,7 +14,7 @@ from chia.types.peer_info import PeerInfo from chia.util.hash import std_hash from chia.util.ints import uint16 from tests.core.fixtures import default_400_blocks, default_1000_blocks, default_10000_blocks, empty_blockchain -from tests.core.node_height import node_height_exactly +from tests.core.node_height import node_height_exactly, node_height_between from tests.setup_nodes import bt, self_hostname, setup_n_nodes, setup_two_nodes, test_constants from tests.time_out_assert import time_out_assert @@ -371,4 +371,4 @@ class TestFullSync: ) await full_node_2.full_node.sync_from_fork_point(0, 500, peak1.header_hash, summaries2) log.info(f"full node height {full_node_2.full_node.blockchain.get_peak().height}") - assert node_height_exactly(full_node_2, 320) + assert node_height_between(full_node_2, 320, 400) diff --git a/tests/core/full_node/test_block_store.py b/tests/core/full_node/test_block_store.py index a8acb4e89f21..63fe3e9306cc 100644 --- a/tests/core/full_node/test_block_store.py +++ b/tests/core/full_node/test_block_store.py @@ -1,4 +1,5 @@ import asyncio +import logging import random import sqlite3 from pathlib import Path @@ -12,6 +13,8 @@ from chia.full_node.coin_store import CoinStore from chia.util.db_wrapper import DBWrapper from tests.setup_nodes import bt, test_constants +log = logging.getLogger(__name__) + @pytest.fixture(scope="module") def event_loop(): diff --git a/tests/core/full_node/test_conditions.py b/tests/core/full_node/test_conditions.py index 6c8d273d69f2..1f9fc8465e8c 100644 --- a/tests/core/full_node/test_conditions.py +++ b/tests/core/full_node/test_conditions.py @@ -6,7 +6,7 @@ or that they're failing for the right reason when they're invalid. import logging import time -from typing import List, Optional +from typing import List, Optional, Tuple import pytest @@ -18,12 +18,14 @@ from chia.consensus.blockchain import ReceiveBlockResult from chia.consensus.constants import ConsensusConstants from chia.types.announcement import Announcement from chia.types.blockchain_format.program import Program +from chia.types.coin_record import CoinRecord from chia.types.coin_solution import CoinSolution from chia.types.condition_opcodes import ConditionOpcode from chia.types.full_block import FullBlock from chia.types.spend_bundle import SpendBundle from tests.block_tools import BlockTools, test_constants from chia.util.errors import Err +from chia.util.ints import uint32 from .ram_db import create_ram_blockchain @@ -56,7 +58,7 @@ async def check_spend_bundle_validity( blocks: List[FullBlock], spend_bundle: SpendBundle, expected_err: Optional[Err] = None, -): +) -> Tuple[List[CoinRecord], List[CoinRecord]]: """ This test helper create an extra block after the given blocks that contains the given `SpendBundle`, and then invokes `receive_block` to ensure that it's accepted (if `expected_err=None`) @@ -78,6 +80,13 @@ async def check_spend_bundle_validity( received_block_result, err, fork_height = await blockchain.receive_block(newest_block) + if fork_height: + coins_added = await blockchain.coin_store.get_coins_added_at_height(uint32(fork_height + 1)) + coins_removed = await blockchain.coin_store.get_coins_removed_at_height(uint32(fork_height + 1)) + else: + coins_added = [] + coins_removed = [] + if expected_err is None: assert err is None assert received_block_result == ReceiveBlockResult.NEW_PEAK @@ -86,6 +95,9 @@ async def check_spend_bundle_validity( assert err == expected_err assert received_block_result == ReceiveBlockResult.INVALID_BLOCK assert fork_height is None + + return coins_added, coins_removed + finally: # if we don't close the connection, the test process doesn't exit cleanly await connection.close() diff --git a/tests/core/full_node/test_full_node.py b/tests/core/full_node/test_full_node.py index 496580213b94..3be4de73f05b 100644 --- a/tests/core/full_node/test_full_node.py +++ b/tests/core/full_node/test_full_node.py @@ -292,7 +292,7 @@ class TestFullNodeBlockCompression: # Creates a cc wallet cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node_1.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node_1.wallet_state_manager.get_send_queue() + tx_queue: List[TransactionRecord] = await wallet_node_1.wallet_state_manager.tx_store.get_not_sent() tr = tx_queue[0] await time_out_assert( 10, @@ -1487,7 +1487,8 @@ class TestFullNodeProtocol: ) ) - assert cc_eos_count == 3 and icc_eos_count == 3 + # Note: the below numbers depend on the block cache, so might need to be updated + assert cc_eos_count == 4 and icc_eos_count == 3 for compact_proof in timelord_protocol_finished: await full_node_1.full_node.respond_compact_proof_of_time(compact_proof) stored_blocks = await full_node_1.get_all_full_blocks() @@ -1508,7 +1509,8 @@ class TestFullNodeProtocol: has_compact_cc_sp_vdf = True if block.challenge_chain_ip_proof.normalized_to_identity: has_compact_cc_ip_vdf = True - assert cc_eos_compact_count == 3 + # Note: the below numbers depend on the block cache, so might need to be updated + assert cc_eos_compact_count == 4 assert icc_eos_compact_count == 3 assert has_compact_cc_sp_vdf assert has_compact_cc_ip_vdf diff --git a/tests/core/full_node/test_full_node_store.py b/tests/core/full_node/test_full_node_store.py index 7ab2cc828ab2..1f73ea9de6c0 100644 --- a/tests/core/full_node/test_full_node_store.py +++ b/tests/core/full_node/test_full_node_store.py @@ -1,5 +1,6 @@ # flake8: noqa: F811, F401 import asyncio +import logging from secrets import token_bytes from typing import List, Optional @@ -15,11 +16,14 @@ from chia.protocols.timelord_protocol import NewInfusionPointVDF from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.unfinished_block import UnfinishedBlock from chia.util.block_cache import BlockCache -from tests.block_tools import get_signage_point +from tests.block_tools import get_signage_point, BlockTools from chia.util.hash import std_hash from chia.util.ints import uint8, uint32, uint64, uint128 -from tests.core.fixtures import default_1000_blocks, empty_blockchain # noqa: F401 -from tests.setup_nodes import bt, test_constants +from tests.core.fixtures import default_1000_blocks, create_blockchain # noqa: F401 +from tests.setup_nodes import test_constants as test_constants_original + +test_constants = test_constants_original.replace(**{"DISCRIMINANT_SIZE_BITS": 32, "SUB_SLOT_ITERS_STARTING": 2 ** 12}) +bt = BlockTools(test_constants) @pytest.fixture(scope="session") @@ -28,6 +32,27 @@ def event_loop(): yield loop +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="function") +async def empty_blockchain(): + bc1, connection, db_path = await create_blockchain(test_constants) + yield bc1 + await connection.close() + bc1.shut_down() + db_path.unlink() + + +@pytest.fixture(scope="function") +async def empty_blockchain_original(): + bc1, connection, db_path = await create_blockchain(test_constants_original) + yield bc1 + await connection.close() + bc1.shut_down() + db_path.unlink() + + class TestFullNodeStore: @pytest.mark.asyncio async def test_basic_store(self, empty_blockchain, normalized_to_identity: bool = False): @@ -576,10 +601,13 @@ class TestFullNodeStore: # SP, B2 SP .... SP B1 # i2 ......... i1 # Then do a reorg up to B2, removing all signage points after B2, but not before + log.warning(f"Adding blocks up to {blocks[-1]}") for block in blocks: await blockchain.receive_block(block) + log.warning(f"Starting loop") while True: + log.warning("Looping") blocks = bt.get_consecutive_blocks(1, block_list_input=blocks, skip_slots=1) assert (await blockchain.receive_block(blocks[-1]))[0] == ReceiveBlockResult.NEW_PEAK peak = blockchain.get_peak() @@ -689,16 +717,16 @@ class TestFullNodeStore: await self.test_basic_store(empty_blockchain, True) @pytest.mark.asyncio - async def test_long_chain_slots(self, empty_blockchain, default_1000_blocks): - blockchain = empty_blockchain - store = FullNodeStore(test_constants) + async def test_long_chain_slots(self, empty_blockchain_original, default_1000_blocks): + blockchain = empty_blockchain_original + store = FullNodeStore(test_constants_original) blocks = default_1000_blocks peak = None peak_full_block = None for block in blocks: for sub_slot in block.finished_sub_slots: assert store.new_finished_sub_slot(sub_slot, blockchain, peak, peak_full_block) is not None - res, _, _ = await blockchain.receive_block(block) + res, err, _ = await blockchain.receive_block(block) assert res == ReceiveBlockResult.NEW_PEAK peak = blockchain.get_peak() peak_full_block = await blockchain.get_full_peak() diff --git a/tests/core/node_height.py b/tests/core/node_height.py index 3c8a72da21ee..68d543770a88 100644 --- a/tests/core/node_height.py +++ b/tests/core/node_height.py @@ -8,3 +8,10 @@ def node_height_exactly(node, h): if node.full_node.blockchain.get_peak() is not None: return node.full_node.blockchain.get_peak().height == h return False + + +def node_height_between(node, h1, h2): + if node.full_node.blockchain.get_peak() is not None: + height = node.full_node.blockchain.get_peak().height + return h1 <= height <= h2 + return False diff --git a/tests/core/test_farmer_harvester_rpc.py b/tests/core/test_farmer_harvester_rpc.py index 435f9d9df501..af82aa928df4 100644 --- a/tests/core/test_farmer_harvester_rpc.py +++ b/tests/core/test_farmer_harvester_rpc.py @@ -1,3 +1,5 @@ +# flake8: noqa: E501 +import logging from secrets import token_bytes import pytest @@ -15,13 +17,16 @@ from chia.rpc.rpc_server import start_rpc_server from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from tests.block_tools import get_plot_dir -from chia.util.config import load_config +from chia.util.byte_types import hexstr_to_bytes +from chia.util.config import load_config, save_config from chia.util.hash import std_hash from chia.util.ints import uint8, uint16, uint32, uint64 -from chia.wallet.derive_keys import master_sk_to_wallet_sk +from chia.wallet.derive_keys import master_sk_to_wallet_sk, master_sk_to_pooling_authentication_sk from tests.setup_nodes import bt, self_hostname, setup_farmer_harvester, test_constants from tests.time_out_assert import time_out_assert +log = logging.getLogger(__name__) + class TestRpc: @pytest.fixture(scope="function") @@ -160,6 +165,10 @@ class TestRpc: res_2 = await client_2.get_plots() assert len(res_2["plots"]) == num_plots + # Test farmer get_plots + farmer_res = await client.get_plots() + assert len(list(farmer_res.values())[0]["plots"]) == num_plots + assert len(await client_2.get_plot_directories()) == 1 await client_2.add_plot_directory(str(plot_dir)) @@ -220,6 +229,35 @@ class TestRpc: with pytest.raises(ValueError): await client.set_reward_targets(None, replaced_char) + assert len((await client.get_pool_state())["pool_state"]) == 0 + all_sks = farmer_api.farmer.keychain.get_all_private_keys() + auth_sk = master_sk_to_pooling_authentication_sk(all_sks[0][0], 2, 1) + pool_list = [ + { + "launcher_id": "ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa", + "authentication_public_key": bytes(auth_sk.get_g1()).hex(), + "owner_public_key": "84c3fcf9d5581c1ddc702cb0f3b4a06043303b334dd993ab42b2c320ebfa98e5ce558448615b3f69638ba92cf7f43da5", + "payout_instructions": "c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8", + "pool_url": "localhost", + "p2_singleton_puzzle_hash": "16e4bac26558d315cded63d4c5860e98deb447cc59146dd4de06ce7394b14f17", + "target_puzzle_hash": "344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58", + } + ] + config["pool"]["pool_list"] = pool_list + save_config(root_path, "config.yaml", config) + await farmer_api.farmer.update_pool_state() + + pool_state = (await client.get_pool_state())["pool_state"] + assert len(pool_state) == 1 + assert ( + pool_state[0]["pool_config"]["payout_instructions"] + == "c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8" + ) + await client.set_payout_instructions(hexstr_to_bytes(pool_state[0]["pool_config"]["launcher_id"]), "1234vy") + + pool_state = (await client.get_pool_state())["pool_state"] + assert pool_state[0]["pool_config"]["payout_instructions"] == "1234vy" + finally: # Checks that the RPC manages to stop the node client.close() diff --git a/tests/core/test_full_node_rpc.py b/tests/core/test_full_node_rpc.py index 3ea58229af1d..fff196030785 100644 --- a/tests/core/test_full_node_rpc.py +++ b/tests/core/test_full_node_rpc.py @@ -1,7 +1,11 @@ +# flake8: noqa: F811, F401 +import logging + import pytest from blspy import AugSchemeMPL from chia.consensus.pot_iterations import is_overflow_block +from chia.full_node.signage_point import SignagePoint from chia.protocols import full_node_protocol from chia.rpc.full_node_rpc_api import FullNodeRpcApi from chia.rpc.full_node_rpc_client import FullNodeRpcClient @@ -9,11 +13,14 @@ from chia.rpc.rpc_server import start_rpc_server from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.spend_bundle import SpendBundle from chia.types.unfinished_block import UnfinishedBlock +from tests.block_tools import get_signage_point from chia.util.hash import std_hash -from chia.util.ints import uint16 +from chia.util.ints import uint16, uint8 from tests.wallet_tools import WalletTool +from tests.connection_utils import connect_and_get_peer from tests.setup_nodes import bt, self_hostname, setup_simulators_and_wallets, test_constants from tests.time_out_assert import time_out_assert +from tests.core.fixtures import empty_blockchain class TestRpc: @@ -138,6 +145,7 @@ class TestRpc: assert (await client.get_mempool_item_by_tx_id(spend_bundle.name())) is None await client.push_tx(spend_bundle) + coin = spend_bundle.additions()[0] assert len(await client.get_all_mempool_items()) == 1 assert len(await client.get_all_mempool_tx_ids()) == 1 @@ -152,9 +160,12 @@ class TestRpc: ) == spend_bundle ) + assert (await client.get_coin_record_by_name(coin.name())) is None await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(ph_2)) + assert (await client.get_coin_record_by_name(coin.name())).coin == coin + assert len(await client.get_coin_records_by_puzzle_hash(ph_receiver)) == 1 assert len(list(filter(lambda cr: not cr.spent, (await client.get_coin_records_by_puzzle_hash(ph))))) == 3 assert len(await client.get_coin_records_by_puzzle_hashes([ph_receiver, ph])) == 5 @@ -183,3 +194,148 @@ class TestRpc: client.close() await client.await_closed() await rpc_cleanup() + + @pytest.mark.asyncio + async def test_signage_points(self, two_nodes, empty_blockchain): + test_rpc_port = uint16(21522) + nodes, _ = two_nodes + full_node_api_1, full_node_api_2 = nodes + server_1 = full_node_api_1.full_node.server + server_2 = full_node_api_2.full_node.server + + peer = await connect_and_get_peer(server_1, server_2) + + def stop_node_cb(): + full_node_api_1._close() + server_1.close_all() + + full_node_rpc_api = FullNodeRpcApi(full_node_api_1.full_node) + + config = bt.config + hostname = config["self_hostname"] + daemon_port = config["daemon_port"] + + rpc_cleanup = await start_rpc_server( + full_node_rpc_api, + hostname, + daemon_port, + test_rpc_port, + stop_node_cb, + bt.root_path, + config, + connect_to_daemon=False, + ) + + try: + client = await FullNodeRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) + + # Only provide one + res = await client.get_recent_signage_point_or_eos(None, None) + assert res is None + res = await client.get_recent_signage_point_or_eos(std_hash(b"0"), std_hash(b"1")) + assert res is None + + # Not found + res = await client.get_recent_signage_point_or_eos(std_hash(b"0"), None) + assert res is None + res = await client.get_recent_signage_point_or_eos(None, std_hash(b"0")) + assert res is None + + blocks = bt.get_consecutive_blocks(5) + for block in blocks: + await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) + + blocks = bt.get_consecutive_blocks(1, block_list_input=blocks, skip_slots=1, force_overflow=True) + + blockchain = full_node_api_1.full_node.blockchain + second_blockchain = empty_blockchain + + for block in blocks: + await second_blockchain.receive_block(block) + + # Creates a signage point based on the last block + peak_2 = second_blockchain.get_peak() + sp: SignagePoint = get_signage_point( + test_constants, + blockchain, + peak_2, + peak_2.ip_sub_slot_total_iters(test_constants), + uint8(4), + [], + peak_2.sub_slot_iters, + ) + + # Don't have SP yet + res = await client.get_recent_signage_point_or_eos(sp.cc_vdf.output.get_hash(), None) + assert res is None + + # Add the last block + await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-1])) + await full_node_api_1.respond_signage_point( + full_node_protocol.RespondSignagePoint(uint8(4), sp.cc_vdf, sp.cc_proof, sp.rc_vdf, sp.rc_proof), peer + ) + + assert full_node_api_1.full_node.full_node_store.get_signage_point(sp.cc_vdf.output.get_hash()) is not None + + # Properly fetch a signage point + res = await client.get_recent_signage_point_or_eos(sp.cc_vdf.output.get_hash(), None) + + assert res is not None + assert "eos" not in res + assert res["signage_point"] == sp + assert not res["reverted"] + + blocks = bt.get_consecutive_blocks(1, block_list_input=blocks, skip_slots=1) + selected_eos = blocks[-1].finished_sub_slots[0] + + # Don't have EOS yet + res = await client.get_recent_signage_point_or_eos(None, selected_eos.challenge_chain.get_hash()) + assert res is None + + # Properly fetch an EOS + for eos in blocks[-1].finished_sub_slots: + await full_node_api_1.full_node.respond_end_of_sub_slot( + full_node_protocol.RespondEndOfSubSlot(eos), peer + ) + + res = await client.get_recent_signage_point_or_eos(None, selected_eos.challenge_chain.get_hash()) + assert res is not None + assert "signage_point" not in res + assert res["eos"] == selected_eos + assert not res["reverted"] + + # Do another one but without sending the slot + await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-1])) + blocks = bt.get_consecutive_blocks(1, block_list_input=blocks, skip_slots=1) + selected_eos = blocks[-1].finished_sub_slots[-1] + await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-1])) + + res = await client.get_recent_signage_point_or_eos(None, selected_eos.challenge_chain.get_hash()) + assert res is not None + assert "signage_point" not in res + assert res["eos"] == selected_eos + assert not res["reverted"] + + # Perform a reorg + blocks = bt.get_consecutive_blocks(12, seed=b"1234") + for block in blocks: + await full_node_api_1.full_node.respond_block(full_node_protocol.RespondBlock(block)) + + # Signage point is no longer in the blockchain + res = await client.get_recent_signage_point_or_eos(sp.cc_vdf.output.get_hash(), None) + assert res["reverted"] + assert res["signage_point"] == sp + assert "eos" not in res + + # EOS is no longer in the blockchain + res = await client.get_recent_signage_point_or_eos(None, selected_eos.challenge_chain.get_hash()) + assert res is not None + assert "signage_point" not in res + assert res["eos"] == selected_eos + assert res["reverted"] + + finally: + # Checks that the RPC manages to stop the node + client.close() + await client.await_closed() + await rpc_cleanup() diff --git a/tests/pools/__init__.py b/tests/pools/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/pools/test_pool_config.py b/tests/pools/test_pool_config.py new file mode 100644 index 000000000000..bed1d2893bb9 --- /dev/null +++ b/tests/pools/test_pool_config.py @@ -0,0 +1,43 @@ +# flake8: noqa: E501 +from pathlib import Path + +from blspy import AugSchemeMPL, PrivateKey + +from chia.pools.pool_config import PoolWalletConfig +from chia.util.config import load_config, save_config, create_default_chia_config + + +def test_pool_config(): + test_root = Path("/tmp") + test_path = Path("/tmp/config") + eg_config = test_path / "config.yaml" + to_config = test_path / "test_pool_config.yaml" + + create_default_chia_config(test_root, ["config.yaml"]) + assert eg_config.exists() + eg_config.rename(to_config) + config = load_config(test_root, "test_pool_config.yaml") + + auth_sk: PrivateKey = AugSchemeMPL.key_gen(b"1" * 32) + d = { + "authentication_public_key": bytes(auth_sk.get_g1()).hex(), + "owner_public_key": "84c3fcf9d5581c1ddc702cb0f3b4a06043303b334dd993ab42b2c320ebfa98e5ce558448615b3f69638ba92cf7f43da5", + "p2_singleton_puzzle_hash": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + "payout_instructions": "c2b08e41d766da4116e388357ed957d04ad754623a915f3fd65188a8746cf3e8", + "pool_url": "localhost", + "launcher_id": "ae4ef3b9bfe68949691281a015a9c16630fc8f66d48c19ca548fb80768791afa", + "target_puzzle_hash": "344587cf06a39db471d2cc027504e8688a0a67cce961253500c956c73603fd58", + } + + pwc = PoolWalletConfig.from_json_dict(d) + + config_a = config.copy() + config_b = config.copy() + + config_a["wallet"]["pool_list"] = [d] + config_b["wallet"]["pool_list"] = [pwc.to_json_dict()] + + print(config["wallet"]["pool_list"]) + save_config(test_root, "test_pool_config_a.yaml", config_a) + save_config(test_root, "test_pool_config_b.yaml", config_b) + assert config_a == config_b diff --git a/tests/pools/test_pool_puzzles_lifecycle.py b/tests/pools/test_pool_puzzles_lifecycle.py new file mode 100644 index 000000000000..76855c83d3c0 --- /dev/null +++ b/tests/pools/test_pool_puzzles_lifecycle.py @@ -0,0 +1,417 @@ +import copy + +from typing import List +from unittest import TestCase + +from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey + +from chia.types.blockchain_format.program import Program +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.blockchain_format.coin import Coin +from chia.types.coin_solution import CoinSolution +from chia.types.spend_bundle import SpendBundle +from chia.util.ints import uint64, uint32 +from chia.consensus.default_constants import DEFAULT_CONSTANTS +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + puzzle_for_pk, + solution_for_conditions, + calculate_synthetic_secret_key, + DEFAULT_HIDDEN_PUZZLE_HASH, +) +from chia.wallet.puzzles.p2_conditions import puzzle_for_conditions +from chia.wallet.puzzles import singleton_top_layer +from chia.pools.pool_wallet_info import PoolState +from chia.pools.pool_puzzles import ( + create_waiting_room_inner_puzzle, + create_pooling_inner_puzzle, + create_p2_singleton_puzzle, + create_absorb_spend, + create_travel_spend, + get_most_recent_singleton_coin_from_coin_solution, + get_delayed_puz_info_from_launcher_spend, + SINGLETON_MOD_HASH, + launcher_id_to_p2_puzzle_hash, + is_pool_singleton_inner_puzzle, + get_pubkey_from_member_inner_puzzle, + solution_to_extra_data, + uncurry_pool_waitingroom_inner_puzzle, + get_seconds_and_delayed_puzhash_from_p2_singleton_puzzle, +) +from tests.util.key_tool import KeyTool +from tests.clvm.test_puzzles import ( + public_key_for_index, + secret_exponent_for_index, +) + +from tests.clvm.coin_store import CoinStore, CoinTimestamp, BadSpendBundleError + +""" +This test suite aims to test: + - chia.pools.pool_puzzles.py + - chia.wallet.puzzles.pool_member_innerpuz.clvm + - chia.wallet.puzzles.pool_waiting_room_innerpuz.clvm +""" + + +# Helper function +def sign_delegated_puz(del_puz: Program, coin: Coin) -> G2Element: + synthetic_secret_key: PrivateKey = calculate_synthetic_secret_key( + PrivateKey.from_bytes( + secret_exponent_for_index(1).to_bytes(32, "big"), + ), + DEFAULT_HIDDEN_PUZZLE_HASH, + ) + return AugSchemeMPL.sign( + synthetic_secret_key, + (del_puz.get_tree_hash() + coin.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), + ) + + +class TestPoolPuzzles(TestCase): + def test_pool_lifecycle(self): + # START TESTS + # Generate starting info + key_lookup = KeyTool() + sk: PrivateKey = PrivateKey.from_bytes( + secret_exponent_for_index(1).to_bytes(32, "big"), + ) + pk: G1Element = G1Element.from_bytes(public_key_for_index(1, key_lookup)) + starting_puzzle: Program = puzzle_for_pk(pk) + starting_ph: bytes32 = starting_puzzle.get_tree_hash() + + # Get our starting standard coin created + START_AMOUNT: uint64 = 1023 + coin_db = CoinStore() + time = CoinTimestamp(10000000, 1) + coin_db.farm_coin(starting_ph, time, START_AMOUNT) + starting_coin: Coin = next(coin_db.all_unspent_coins()) + + # LAUNCHING + # Create the escaping inner puzzle + GENESIS_CHALLENGE = bytes32.fromhex("ccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb") + launcher_coin = singleton_top_layer.generate_launcher_coin( + starting_coin, + START_AMOUNT, + ) + DELAY_TIME = uint64(60800) + DELAY_PH = starting_ph + launcher_id = launcher_coin.name() + relative_lock_height: uint32 = uint32(5000) + # use a dummy pool state + pool_state = PoolState( + owner_pubkey=pk, + pool_url="", + relative_lock_height=relative_lock_height, + state=3, # farming to pool + target_puzzle_hash=starting_ph, + version=1, + ) + # create a new dummy pool state for travelling + target_pool_state = PoolState( + owner_pubkey=pk, + pool_url="", + relative_lock_height=relative_lock_height, + state=2, # Leaving pool + target_puzzle_hash=starting_ph, + version=1, + ) + # Standard format comment + comment = Program.to([("p", bytes(pool_state)), ("t", DELAY_TIME), ("h", DELAY_PH)]) + pool_wr_innerpuz: bytes32 = create_waiting_room_inner_puzzle( + starting_ph, + relative_lock_height, + pk, + launcher_id, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, + ) + pool_wr_inner_hash = pool_wr_innerpuz.get_tree_hash() + pooling_innerpuz: Program = create_pooling_inner_puzzle( + starting_ph, + pool_wr_inner_hash, + pk, + launcher_id, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, + ) + # Driver tests + assert is_pool_singleton_inner_puzzle(pooling_innerpuz) + assert is_pool_singleton_inner_puzzle(pool_wr_innerpuz) + assert get_pubkey_from_member_inner_puzzle(pooling_innerpuz) == pk + # Generating launcher information + conditions, launcher_coinsol = singleton_top_layer.launch_conditions_and_coinsol( + starting_coin, pooling_innerpuz, comment, START_AMOUNT + ) + # Creating solution for standard transaction + delegated_puzzle: Program = puzzle_for_conditions(conditions) + full_solution: Program = solution_for_conditions(conditions) + starting_coinsol = CoinSolution( + starting_coin, + starting_puzzle, + full_solution, + ) + # Create the spend bundle + sig: G2Element = sign_delegated_puz(delegated_puzzle, starting_coin) + spend_bundle = SpendBundle( + [starting_coinsol, launcher_coinsol], + sig, + ) + # Spend it! + coin_db.update_coin_store_for_spend_bundle( + spend_bundle, + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + # Test that we can retrieve the extra data + assert get_delayed_puz_info_from_launcher_spend(launcher_coinsol) == (DELAY_TIME, DELAY_PH) + assert solution_to_extra_data(launcher_coinsol) == pool_state + + # TEST TRAVEL AFTER LAUNCH + # fork the state + fork_coin_db: CoinStore = copy.deepcopy(coin_db) + post_launch_coinsol, _ = create_travel_spend( + launcher_coinsol, + launcher_coin, + pool_state, + target_pool_state, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, + ) + # Spend it! + fork_coin_db.update_coin_store_for_spend_bundle( + SpendBundle([post_launch_coinsol], G2Element()), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + + # HONEST ABSORB + time = CoinTimestamp(10000030, 2) + # create the farming reward + p2_singleton_puz: Program = create_p2_singleton_puzzle( + SINGLETON_MOD_HASH, + launcher_id, + DELAY_TIME, + DELAY_PH, + ) + p2_singleton_ph: bytes32 = p2_singleton_puz.get_tree_hash() + assert uncurry_pool_waitingroom_inner_puzzle(pool_wr_innerpuz) == ( + starting_ph, + relative_lock_height, + pk, + p2_singleton_ph, + ) + assert launcher_id_to_p2_puzzle_hash(launcher_id, DELAY_TIME, DELAY_PH) == p2_singleton_ph + assert get_seconds_and_delayed_puzhash_from_p2_singleton_puzzle(p2_singleton_puz) == (DELAY_TIME, DELAY_PH) + coin_db.farm_coin(p2_singleton_ph, time, 1750000000000) + coin_sols: List[CoinSolution] = create_absorb_spend( + launcher_coinsol, + pool_state, + launcher_coin, + 2, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, # height + ) + # Spend it! + coin_db.update_coin_store_for_spend_bundle( + SpendBundle(coin_sols, G2Element()), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + + # ABSORB A NON EXISTENT REWARD (Negative test) + last_coinsol: CoinSolution = list( + filter( + lambda e: e.coin.amount == START_AMOUNT, + coin_sols, + ) + )[0] + coin_sols: List[CoinSolution] = create_absorb_spend( + last_coinsol, + pool_state, + launcher_coin, + 2, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, # height + ) + # filter for only the singleton solution + singleton_coinsol: CoinSolution = list( + filter( + lambda e: e.coin.amount == START_AMOUNT, + coin_sols, + ) + )[0] + # Spend it and hope it fails! + try: + coin_db.update_coin_store_for_spend_bundle( + SpendBundle([singleton_coinsol], G2Element()), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + except BadSpendBundleError as e: + assert str(e) == "condition validation failure Err.ASSERT_ANNOUNCE_CONSUMED_FAILED" + + # SPEND A NON-REWARD P2_SINGLETON (Negative test) + # create the dummy coin + non_reward_p2_singleton = Coin( + bytes32(32 * b"3"), + p2_singleton_ph, + uint64(1337), + ) + coin_db._add_coin_entry(non_reward_p2_singleton, time) + # construct coin solution for the p2_singleton coin + bad_coinsol = CoinSolution( + non_reward_p2_singleton, + p2_singleton_puz, + Program.to( + [ + pooling_innerpuz.get_tree_hash(), + non_reward_p2_singleton.name(), + ] + ), + ) + # Spend it and hope it fails! + try: + coin_db.update_coin_store_for_spend_bundle( + SpendBundle([singleton_coinsol, bad_coinsol], G2Element()), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + except BadSpendBundleError as e: + assert str(e) == "condition validation failure Err.ASSERT_ANNOUNCE_CONSUMED_FAILED" + + # ENTER WAITING ROOM + # find the singleton + singleton = get_most_recent_singleton_coin_from_coin_solution(last_coinsol) + # get the relevant coin solution + travel_coinsol, _ = create_travel_spend( + last_coinsol, + launcher_coin, + pool_state, + target_pool_state, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, + ) + # Test that we can retrieve the extra data + assert solution_to_extra_data(travel_coinsol) == target_pool_state + # sign the serialized state + data = Program.to(bytes(target_pool_state)).get_tree_hash() + sig: G2Element = AugSchemeMPL.sign( + sk, + (data + singleton.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), + ) + # Spend it! + coin_db.update_coin_store_for_spend_bundle( + SpendBundle([travel_coinsol], sig), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + + # ESCAPE TOO FAST (Negative test) + # find the singleton + singleton = get_most_recent_singleton_coin_from_coin_solution(travel_coinsol) + # get the relevant coin solution + return_coinsol, _ = create_travel_spend( + travel_coinsol, + launcher_coin, + target_pool_state, + pool_state, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, + ) + # sign the serialized target state + sig = AugSchemeMPL.sign( + sk, + (data + singleton.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), + ) + # Spend it and hope it fails! + try: + coin_db.update_coin_store_for_spend_bundle( + SpendBundle([return_coinsol], sig), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + except BadSpendBundleError as e: + assert str(e) == "condition validation failure Err.ASSERT_HEIGHT_RELATIVE_FAILED" + + # ABSORB WHILE IN WAITING ROOM + time = CoinTimestamp(10000060, 3) + # create the farming reward + coin_db.farm_coin(p2_singleton_ph, time, 1750000000000) + # generate relevant coin solutions + coin_sols: List[CoinSolution] = create_absorb_spend( + travel_coinsol, + target_pool_state, + launcher_coin, + 3, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, # height + ) + # Spend it! + coin_db.update_coin_store_for_spend_bundle( + SpendBundle(coin_sols, G2Element()), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + + # LEAVE THE WAITING ROOM + time = CoinTimestamp(20000000, 10000) + # find the singleton + singleton_coinsol: CoinSolution = list( + filter( + lambda e: e.coin.amount == START_AMOUNT, + coin_sols, + ) + )[0] + singleton: Coin = get_most_recent_singleton_coin_from_coin_solution(singleton_coinsol) + # get the relevant coin solution + return_coinsol, _ = create_travel_spend( + singleton_coinsol, + launcher_coin, + target_pool_state, + pool_state, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, + ) + # Test that we can retrieve the extra data + assert solution_to_extra_data(return_coinsol) == pool_state + # sign the serialized target state + data = Program.to([pooling_innerpuz.get_tree_hash(), START_AMOUNT, bytes(pool_state)]).get_tree_hash() + sig: G2Element = AugSchemeMPL.sign( + sk, + (data + singleton.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), + ) + # Spend it! + coin_db.update_coin_store_for_spend_bundle( + SpendBundle([return_coinsol], sig), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) + + # ABSORB ONCE MORE FOR GOOD MEASURE + time = CoinTimestamp(20000000, 10005) + # create the farming reward + coin_db.farm_coin(p2_singleton_ph, time, 1750000000000) + coin_sols: List[CoinSolution] = create_absorb_spend( + return_coinsol, + pool_state, + launcher_coin, + 10005, + GENESIS_CHALLENGE, + DELAY_TIME, + DELAY_PH, # height + ) + # Spend it! + coin_db.update_coin_store_for_spend_bundle( + SpendBundle(coin_sols, G2Element()), + time, + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, + ) diff --git a/tests/pools/test_pool_rpc.py b/tests/pools/test_pool_rpc.py new file mode 100644 index 000000000000..76fffc0cbb92 --- /dev/null +++ b/tests/pools/test_pool_rpc.py @@ -0,0 +1,851 @@ +# flake8: noqa: E501 +import asyncio +import logging +import os +from argparse import Namespace +from typing import Optional, List, Dict + +import pytest +from blspy import G1Element, AugSchemeMPL + +from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward +from chia.plotting.create_plots import create_plots +from chia.pools.pool_wallet_info import PoolWalletInfo, PoolSingletonState +from chia.protocols import full_node_protocol +from chia.rpc.rpc_server import start_rpc_server +from chia.rpc.wallet_rpc_api import WalletRpcApi +from chia.rpc.wallet_rpc_client import WalletRpcClient +from chia.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol +from chia.types.blockchain_format.proof_of_space import ProofOfSpace +from chia.types.blockchain_format.sized_bytes import bytes32 + +from chia.types.peer_info import PeerInfo +from chia.util.bech32m import encode_puzzle_hash +from tests.block_tools import get_plot_dir, get_plot_tmp_dir +from chia.util.config import load_config +from chia.util.hash import std_hash +from chia.util.ints import uint16, uint32 +from chia.wallet.derive_keys import master_sk_to_local_sk +from chia.wallet.transaction_record import TransactionRecord +from chia.wallet.util.wallet_types import WalletType +from tests.setup_nodes import self_hostname, setup_simulators_and_wallets, bt +from tests.time_out_assert import time_out_assert + + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +class TestPoolWalletRpc: + @pytest.fixture(scope="function") + async def two_wallet_nodes(self): + async for _ in setup_simulators_and_wallets(1, 2, {}): + yield _ + + @pytest.fixture(scope="function") + async def one_wallet_node_and_rpc(self): + async for nodes in setup_simulators_and_wallets(1, 1, {}): + full_nodes, wallets = nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, wallet_server_0 = wallets[0] + await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + await self.farm_blocks(full_node_api, our_ph, 4) + total_block_rewards = await self.get_total_block_rewards(4) + + await time_out_assert(10, wallet_0.get_confirmed_balance, total_block_rewards) + api_user = WalletRpcApi(wallet_node_0) + config = bt.config + hostname = config["self_hostname"] + daemon_port = config["daemon_port"] + test_rpc_port = uint16(21529) + + rpc_cleanup = await start_rpc_server( + api_user, + hostname, + daemon_port, + test_rpc_port, + lambda x: None, + bt.root_path, + config, + connect_to_daemon=False, + ) + client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) + + yield client, wallet_node_0, full_node_api + + client.close() + await client.await_closed() + await rpc_cleanup() + + @pytest.fixture(scope="function") + async def setup(self, two_wallet_nodes): + full_nodes, wallets = two_wallet_nodes + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, wallet_server_0 = wallets[0] + wallet_node_1, wallet_server_1 = wallets[1] + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + wallet_1 = wallet_node_1.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + pool_ph = await wallet_1.get_new_puzzlehash() + + await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + api_user = WalletRpcApi(wallet_node_0) + config = bt.config + hostname = config["self_hostname"] + daemon_port = config["daemon_port"] + test_rpc_port = uint16(21529) + + rpc_cleanup = await start_rpc_server( + api_user, + hostname, + daemon_port, + test_rpc_port, + lambda x: None, + bt.root_path, + config, + connect_to_daemon=False, + ) + client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config) + + return ( + full_nodes, + [wallet_0, wallet_1], + [our_ph, pool_ph], + client, # wallet rpc client + rpc_cleanup, + ) + + async def get_total_block_rewards(self, num_blocks): + funds = sum( + [calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)] + ) + return funds + + async def farm_blocks(self, full_node_api, ph: bytes32, num_blocks: int): + for i in range(num_blocks): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + return num_blocks + # TODO also return calculated block rewards + + def create_pool_plot(self, p2_singleton_puzzle_hash: bytes32, shuil=None) -> bytes32: + plot_dir = get_plot_dir() + temp_dir = get_plot_tmp_dir() + args = Namespace() + args.size = 22 + args.num = 1 + args.buffer = 100 + args.farmer_public_key = bytes(bt.farmer_pk).hex() + args.pool_public_key = None + args.pool_contract_address = encode_puzzle_hash(p2_singleton_puzzle_hash, "txch") + args.tmp_dir = temp_dir + args.tmp2_dir = plot_dir + args.final_dir = plot_dir + args.plotid = None + args.memo = None + args.buckets = 0 + args.stripe_size = 2000 + args.num_threads = 0 + args.nobitfield = False + args.exclude_final_dir = False + args.list_duplicates = False + test_private_keys = [AugSchemeMPL.key_gen(std_hash(b"test_pool_rpc"))] + plot_public_key = ProofOfSpace.generate_plot_public_key( + master_sk_to_local_sk(test_private_keys[0]).get_g1(), bt.farmer_pk, True + ) + plot_id = ProofOfSpace.calculate_plot_id_ph(p2_singleton_puzzle_hash, plot_public_key) + try: + create_plots( + args, + bt.root_path, + use_datetime=False, + test_private_keys=test_private_keys, + ) + except KeyboardInterrupt: + shuil.rmtree(plot_dir, ignore_errors=True) + raise + bt.load_plots() + return plot_id + + def delete_plot(self, plot_id: bytes32): + for child in get_plot_dir().iterdir(): + if not child.is_dir() and plot_id.hex() in child.name: + os.remove(child) + + @pytest.mark.asyncio + async def test_create_new_pool_wallet_self_farm(self, one_wallet_node_and_rpc): + client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING" + ) + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 6) + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + + summaries_response = await client.get_wallets() + wallet_id: Optional[int] = None + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + wallet_id = summary["id"] + assert wallet_id is not None + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + assert status.current.state == PoolSingletonState.SELF_POOLING.value + assert status.target is None + assert status.current.owner_pubkey == G1Element.from_bytes( + bytes.fromhex( + "b286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304" + ) + ) + assert status.current.pool_url is None + assert status.current.relative_lock_height == 0 + assert status.current.version == 1 + # Check that config has been written properly + full_config: Dict = load_config(wallet_0.wallet_state_manager.root_path, "config.yaml") + pool_list: List[Dict] = full_config["pool"]["pool_list"] + assert len(pool_list) == 1 + pool_config = pool_list[0] + assert ( + pool_config["authentication_public_key"] + == "0xb3c4b513600729c6b2cf776d8786d620b6acc88f86f9d6f489fa0a0aff81d634262d5348fb7ba304db55185bb4c5c8a4" + ) + # It can be one of multiple launcher IDs, due to selecting a different coin + assert pool_config["launcher_id"] in { + "0x78a1eadf583a2f27a129d7aeba076ec6a5200e1ec8225a72c9d4180342bf91a7", + "0x2bcab0310e78a7ab04e251ac6bdd5dfc80ce6895132e64f97265029db3d8309a", + "0x09edf686c318c138cd3461c38e9b4e10e7f21fc476a0929b4480e126b6efcb81", + } + assert pool_config["pool_url"] == "" + + @pytest.mark.asyncio + async def test_create_new_pool_wallet_farm_to_pool(self, one_wallet_node_and_rpc): + client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "http://pool.example.com", 10, "localhost:5000", "new", "FARMING_TO_POOL" + ) + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 6) + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + + summaries_response = await client.get_wallets() + wallet_id: Optional[int] = None + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + wallet_id = summary["id"] + assert wallet_id is not None + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value + assert status.target is None + assert status.current.owner_pubkey == G1Element.from_bytes( + bytes.fromhex( + "b286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304" + ) + ) + assert status.current.pool_url == "http://pool.example.com" + assert status.current.relative_lock_height == 10 + assert status.current.version == 1 + # Check that config has been written properly + full_config: Dict = load_config(wallet_0.wallet_state_manager.root_path, "config.yaml") + pool_list: List[Dict] = full_config["pool"]["pool_list"] + assert len(pool_list) == 1 + pool_config = pool_list[0] + assert ( + pool_config["authentication_public_key"] + == "0xb3c4b513600729c6b2cf776d8786d620b6acc88f86f9d6f489fa0a0aff81d634262d5348fb7ba304db55185bb4c5c8a4" + ) + # It can be one of multiple launcher IDs, due to selecting a different coin + assert pool_config["launcher_id"] in { + "0x78a1eadf583a2f27a129d7aeba076ec6a5200e1ec8225a72c9d4180342bf91a7", + "0x2bcab0310e78a7ab04e251ac6bdd5dfc80ce6895132e64f97265029db3d8309a", + "0x09edf686c318c138cd3461c38e9b4e10e7f21fc476a0929b4480e126b6efcb81", + } + assert pool_config["pool_url"] == "http://pool.example.com" + + @pytest.mark.asyncio + async def test_create_multiple_pool_wallets(self, one_wallet_node_and_rpc): + client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph_1 = await wallet_0.get_new_puzzlehash() + our_ph_2 = await wallet_0.get_new_puzzlehash() + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph_1, "", 0, "localhost:5000", "new", "SELF_POOLING" + ) + creation_tx_2: TransactionRecord = await client.create_new_pool_wallet( + our_ph_1, "localhost", 12, "localhost:5000", "new", "FARMING_TO_POOL" + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx_2.spend_bundle, + creation_tx_2.name, + ) + + await self.farm_blocks(full_node_api, our_ph_2, 6) + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx_2.name) is None + + await asyncio.sleep(3) + status_2: PoolWalletInfo = (await client.pw_status(2))[0] + status_3: PoolWalletInfo = (await client.pw_status(3))[0] + + if status_2.current.state == PoolSingletonState.SELF_POOLING.value: + assert status_3.current.state == PoolSingletonState.FARMING_TO_POOL.value + else: + assert status_2.current.state == PoolSingletonState.FARMING_TO_POOL.value + assert status_3.current.state == PoolSingletonState.SELF_POOLING.value + + full_config: Dict = load_config(wallet_0.wallet_state_manager.root_path, "config.yaml") + pool_list: List[Dict] = full_config["pool"]["pool_list"] + assert len(pool_list) == 2 + + p2_singleton_ph_2: bytes32 = status_2.p2_singleton_puzzle_hash + p2_singleton_ph_3: bytes32 = status_3.p2_singleton_puzzle_hash + assert ( + await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( + p2_singleton_ph_2 + ) + ) is not None + assert ( + await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( + p2_singleton_ph_3 + ) + ) is not None + assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 + assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(3)) == 0 + # Doing a reorg reverts and removes the pool wallets + await full_node_api.reorg_from_index_to_new_index(ReorgProtocol(uint32(0), uint32(20), our_ph_2)) + await asyncio.sleep(5) + summaries_response = await client.get_wallets() + assert len(summaries_response) == 1 + + with pytest.raises(ValueError): + await client.pw_status(2) + with pytest.raises(ValueError): + await client.pw_status(3) + # It also removed interested PH, so we can recreated the pool wallet with another wallet_id later + assert ( + await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( + p2_singleton_ph_2 + ) + ) is None + assert ( + await wallet_node_0.wallet_state_manager.interested_store.get_interested_puzzle_hash_wallet_id( + p2_singleton_ph_3 + ) + ) is None + + @pytest.mark.asyncio + async def test_absorb_self(self, one_wallet_node_and_rpc): + client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING" + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + await self.farm_blocks(full_node_api, our_ph, 1) + await asyncio.sleep(2) + status: PoolWalletInfo = (await client.pw_status(2))[0] + + assert status.current.state == PoolSingletonState.SELF_POOLING.value + plot_id: bytes32 = self.create_pool_plot(status.p2_singleton_puzzle_hash) + all_blocks = await full_node_api.get_all_full_blocks() + blocks = bt.get_consecutive_blocks( + 3, + block_list_input=all_blocks, + force_plot_id=plot_id, + farmer_reward_puzzle_hash=our_ph, + guarantee_transaction_block=True, + ) + + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-3])) + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-2])) + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-1])) + await asyncio.sleep(2) + + bal = await client.get_wallet_balance(2) + assert bal["confirmed_wallet_balance"] == 2 * 1750000000000 + + # Claim 2 * 1.75, and farm a new 1.75 + absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2) + await time_out_assert( + 5, + full_node_api.full_node.mempool_manager.get_spendbundle, + absorb_tx.spend_bundle, + absorb_tx.name, + ) + await self.farm_blocks(full_node_api, our_ph, 2) + await asyncio.sleep(2) + new_status: PoolWalletInfo = (await client.pw_status(2))[0] + assert status.current == new_status.current + assert status.tip_singleton_coin_id != new_status.tip_singleton_coin_id + bal = await client.get_wallet_balance(2) + assert bal["confirmed_wallet_balance"] == 1 * 1750000000000 + + # Claim another 1.75 + absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2) + absorb_tx.spend_bundle.debug() + await time_out_assert( + 5, + full_node_api.full_node.mempool_manager.get_spendbundle, + absorb_tx.spend_bundle, + absorb_tx.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 2) + await asyncio.sleep(2) + bal = await client.get_wallet_balance(2) + assert bal["confirmed_wallet_balance"] == 0 + self.delete_plot(plot_id) + + assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 + + @pytest.mark.asyncio + async def test_absorb_pooling(self, one_wallet_node_and_rpc): + client, wallet_node_0, full_node_api = one_wallet_node_and_rpc + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + our_ph = await wallet_0.get_new_puzzlehash() + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + # Balance stars at 6 XCH + assert (await wallet_0.get_confirmed_balance()) == 6000000000000 + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "http://123.45.67.89", 10, "localhost:5000", "new", "FARMING_TO_POOL" + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + await self.farm_blocks(full_node_api, our_ph, 1) + await asyncio.sleep(2) + status: PoolWalletInfo = (await client.pw_status(2))[0] + + log.warning(f"{await wallet_0.get_confirmed_balance()}") + assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value + plot_id: bytes32 = self.create_pool_plot(status.p2_singleton_puzzle_hash) + all_blocks = await full_node_api.get_all_full_blocks() + blocks = bt.get_consecutive_blocks( + 3, + block_list_input=all_blocks, + force_plot_id=plot_id, + farmer_reward_puzzle_hash=our_ph, + guarantee_transaction_block=True, + ) + + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-3])) + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-2])) + await full_node_api.full_node.respond_block(full_node_protocol.RespondBlock(blocks[-1])) + await asyncio.sleep(2) + bal = await client.get_wallet_balance(2) + log.warning(f"{await wallet_0.get_confirmed_balance()}") + # Pooled plots don't have balance + assert bal["confirmed_wallet_balance"] == 0 + + # Claim 2 * 1.75, and farm a new 1.75 + absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2) + await time_out_assert( + 5, + full_node_api.full_node.mempool_manager.get_spendbundle, + absorb_tx.spend_bundle, + absorb_tx.name, + ) + await self.farm_blocks(full_node_api, our_ph, 2) + await asyncio.sleep(2) + new_status: PoolWalletInfo = (await client.pw_status(2))[0] + assert status.current == new_status.current + assert status.tip_singleton_coin_id != new_status.tip_singleton_coin_id + bal = await client.get_wallet_balance(2) + log.warning(f"{await wallet_0.get_confirmed_balance()}") + assert bal["confirmed_wallet_balance"] == 0 + + # Claim another 1.75 + absorb_tx: TransactionRecord = await client.pw_absorb_rewards(2) + absorb_tx.spend_bundle.debug() + await time_out_assert( + 5, + full_node_api.full_node.mempool_manager.get_spendbundle, + absorb_tx.spend_bundle, + absorb_tx.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 2) + await asyncio.sleep(2) + bal = await client.get_wallet_balance(2) + assert bal["confirmed_wallet_balance"] == 0 + log.warning(f"{await wallet_0.get_confirmed_balance()}") + self.delete_plot(plot_id) + assert len(await wallet_node_0.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 + assert ( + wallet_node_0.wallet_state_manager.get_peak().height == full_node_api.full_node.blockchain.get_peak().height + ) + # Balance stars at 6 XCH and 5 more blocks are farmed, total 22 XCH + assert (await wallet_0.get_confirmed_balance()) == 21999999999999 + + @pytest.mark.asyncio + async def test_self_pooling_to_pooling(self, setup): + """This tests self-pooling -> pooling""" + num_blocks = 4 # Num blocks to farm at a time + total_blocks = 0 # Total blocks farmed so far + full_nodes, wallets, receive_address, client, rpc_cleanup = setup + our_ph = receive_address[0] + pool_ph = receive_address[1] + full_node_api = full_nodes[0] + + try: + total_blocks += await self.farm_blocks(full_node_api, our_ph, num_blocks) + total_block_rewards = await self.get_total_block_rewards(total_blocks) + + await time_out_assert(10, wallets[0].get_unconfirmed_balance, total_block_rewards) + await time_out_assert(10, wallets[0].get_confirmed_balance, total_block_rewards) + await time_out_assert(10, wallets[0].get_spendable_balance, total_block_rewards) + assert total_block_rewards > 0 + + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING" + ) + creation_tx_2: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING" + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx_2.spend_bundle, + creation_tx_2.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 6) + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + + summaries_response = await client.get_wallets() + wallet_id: Optional[int] = None + wallet_id_2: Optional[int] = None + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + if wallet_id is not None: + wallet_id_2 = summary["id"] + else: + wallet_id = summary["id"] + assert wallet_id is not None + assert wallet_id_2 is not None + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + status_2: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + assert status.current.state == PoolSingletonState.SELF_POOLING.value + assert status_2.current.state == PoolSingletonState.SELF_POOLING.value + assert status.target is None + assert status_2.target is None + + join_pool_tx: TransactionRecord = await client.pw_join_pool( + wallet_id, + pool_ph, + "https://pool.example.com", + 10, + ) + join_pool_tx_2: TransactionRecord = await client.pw_join_pool( + wallet_id_2, + pool_ph, + "https://pool.example.com", + 10, + ) + assert join_pool_tx is not None + assert join_pool_tx_2 is not None + + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + status_2: PoolWalletInfo = (await client.pw_status(wallet_id_2))[0] + + assert status.current.state == PoolSingletonState.SELF_POOLING.value + assert status.target is not None + assert status.target.state == PoolSingletonState.FARMING_TO_POOL.value + assert status_2.current.state == PoolSingletonState.SELF_POOLING.value + assert status_2.target is not None + assert status_2.target.state == PoolSingletonState.FARMING_TO_POOL.value + + await self.farm_blocks(full_node_api, our_ph, 6) + + total_blocks += await self.farm_blocks(full_node_api, our_ph, num_blocks) + + async def status_is_farming_to_pool(w_id: int): + pw_status: PoolWalletInfo = (await client.pw_status(w_id))[0] + return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value + + await time_out_assert(20, status_is_farming_to_pool, True, wallet_id) + await time_out_assert(20, status_is_farming_to_pool, True, wallet_id_2) + assert len(await wallets[0].wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 + + finally: + client.close() + await client.await_closed() + await rpc_cleanup() + + @pytest.mark.asyncio + async def test_leave_pool(self, setup): + """This tests self-pooling -> pooling -> escaping -> self pooling""" + full_nodes, wallets, receive_address, client, rpc_cleanup = setup + our_ph = receive_address[0] + pool_ph = receive_address[1] + full_node_api = full_nodes[0] + WAIT_SECS = 200 + + try: + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + async def have_chia(): + await self.farm_blocks(full_node_api, our_ph, 1) + return (await wallets[0].get_confirmed_balance()) > 0 + + await time_out_assert(timeout=WAIT_SECS, function=have_chia) + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + our_ph, "", 0, "localhost:5000", "new", "SELF_POOLING" + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 6) + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + + summaries_response = await client.get_wallets() + wallet_id: Optional[int] = None + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + wallet_id = summary["id"] + assert wallet_id is not None + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + assert status.current.state == PoolSingletonState.SELF_POOLING.value + assert status.target is None + + join_pool_tx: TransactionRecord = await client.pw_join_pool( + wallet_id, + pool_ph, + "https://pool.example.com", + 5, + ) + assert join_pool_tx is not None + + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + assert status.current.state == PoolSingletonState.SELF_POOLING.value + assert status.current.to_json_dict() == { + "owner_pubkey": "0xb286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304", + "pool_url": None, + "relative_lock_height": 0, + "state": 1, + "target_puzzle_hash": "0x738127e26cb61ffe5530ce0cef02b5eeadb1264aa423e82204a6d6bf9f31c2b7", + "version": 1, + } + assert status.target.to_json_dict() == { + "owner_pubkey": "0xb286bbf7a10fa058d2a2a758921377ef00bb7f8143e1bd40dd195ae918dbef42cfc481140f01b9eae13b430a0c8fe304", + "pool_url": "https://pool.example.com", + "relative_lock_height": 5, + "state": 3, + "target_puzzle_hash": "0x9ba327777484b8300d60427e4f3b776ac81948dfedd069a8d3f55834e101696e", + "version": 1, + } + + async def status_is_farming_to_pool(): + await self.farm_blocks(full_node_api, our_ph, 1) + pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value + + await time_out_assert(timeout=WAIT_SECS, function=status_is_farming_to_pool) + + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + leave_pool_tx: TransactionRecord = await client.pw_self_pool(wallet_id) + assert leave_pool_tx.wallet_id == wallet_id + assert leave_pool_tx.amount == 1 + + async def status_is_leaving(): + await self.farm_blocks(full_node_api, our_ph, 1) + pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + return pw_status.current.state == PoolSingletonState.LEAVING_POOL.value + + await time_out_assert(timeout=WAIT_SECS, function=status_is_leaving) + pw_info: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + async def status_is_self_pooling(): + # Farm enough blocks to wait for relative_lock_height + await self.farm_blocks(full_node_api, our_ph, 1) + pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + return pw_status.current.state == PoolSingletonState.SELF_POOLING.value + + await time_out_assert(timeout=WAIT_SECS, function=status_is_self_pooling) + pw_info: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + assert len(await wallets[0].wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 + + finally: + client.close() + await client.await_closed() + await rpc_cleanup() + + @pytest.mark.asyncio + async def test_change_pools(self, setup): + """This tests Pool A -> escaping -> Pool B""" + full_nodes, wallets, receive_address, client, rpc_cleanup = setup + our_ph = receive_address[0] + pool_a_ph = receive_address[1] + pool_b_ph = await wallets[1].get_new_puzzlehash() + + full_node_api = full_nodes[0] + WAIT_SECS = 200 + + try: + summaries_response = await client.get_wallets() + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + assert False + + async def have_chia(): + await self.farm_blocks(full_node_api, our_ph, 1) + return (await wallets[0].get_confirmed_balance()) > 0 + + await time_out_assert(timeout=WAIT_SECS, function=have_chia) + + creation_tx: TransactionRecord = await client.create_new_pool_wallet( + pool_a_ph, "https://pool-a.org", 5, "localhost:5000", "new", "FARMING_TO_POOL" + ) + + await time_out_assert( + 10, + full_node_api.full_node.mempool_manager.get_spendbundle, + creation_tx.spend_bundle, + creation_tx.name, + ) + + await self.farm_blocks(full_node_api, our_ph, 6) + assert full_node_api.full_node.mempool_manager.get_spendbundle(creation_tx.name) is None + + summaries_response = await client.get_wallets() + wallet_id: Optional[int] = None + for summary in summaries_response: + if WalletType(int(summary["type"])) == WalletType.POOLING_WALLET: + wallet_id = summary["id"] + assert wallet_id is not None + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + assert status.current.state == PoolSingletonState.FARMING_TO_POOL.value + assert status.target is None + + async def status_is_farming_to_pool(): + await self.farm_blocks(full_node_api, our_ph, 1) + pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + return pw_status.current.state == PoolSingletonState.FARMING_TO_POOL.value + + await time_out_assert(timeout=WAIT_SECS, function=status_is_farming_to_pool) + + pw_info: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + assert pw_info.current.pool_url == "https://pool-a.org" + assert pw_info.current.relative_lock_height == 5 + status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + join_pool_tx: TransactionRecord = await client.pw_join_pool( + wallet_id, + pool_b_ph, + "https://pool-b.org", + 10, + ) + assert join_pool_tx is not None + + async def status_is_leaving(): + await self.farm_blocks(full_node_api, our_ph, 1) + pw_status: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + return pw_status.current.state == PoolSingletonState.LEAVING_POOL.value + + await time_out_assert(timeout=WAIT_SECS, function=status_is_leaving) + pw_info: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + + await time_out_assert(timeout=WAIT_SECS, function=status_is_farming_to_pool) + pw_info: PoolWalletInfo = (await client.pw_status(wallet_id))[0] + assert pw_info.current.pool_url == "https://pool-b.org" + assert pw_info.current.relative_lock_height == 10 + assert len(await wallets[0].wallet_state_manager.tx_store.get_unconfirmed_for_wallet(2)) == 0 + + finally: + client.close() + await client.await_closed() + await rpc_cleanup() diff --git a/tests/pools/test_pool_wallet.py b/tests/pools/test_pool_wallet.py new file mode 100644 index 000000000000..61a1b310091d --- /dev/null +++ b/tests/pools/test_pool_wallet.py @@ -0,0 +1,70 @@ +import asyncio +import logging +from typing import List + +import pytest +from blspy import PrivateKey + +from chia.pools.pool_wallet import PoolWallet +from chia.pools.pool_wallet_info import PoolState, FARMING_TO_POOL +from chia.simulator.simulator_protocol import FarmNewBlockProtocol +from chia.types.coin_solution import CoinSolution +from chia.types.full_block import FullBlock +from chia.types.peer_info import PeerInfo +from chia.util.ints import uint16, uint32 +from chia.wallet.derive_keys import master_sk_to_singleton_owner_sk +from chia.wallet.wallet_state_manager import WalletStateManager +from tests.setup_nodes import self_hostname, setup_simulators_and_wallets + + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +class TestPoolWallet2: + @pytest.fixture(scope="function") + async def one_wallet_node(self): + async for _ in setup_simulators_and_wallets(1, 1, {}): + yield _ + + @pytest.mark.asyncio + async def test_create_new_pool_wallet(self, one_wallet_node): + full_nodes, wallets = one_wallet_node + full_node_api = full_nodes[0] + full_node_server = full_node_api.server + wallet_node_0, wallet_server_0 = wallets[0] + wsm: WalletStateManager = wallet_node_0.wallet_state_manager + + wallet_0 = wallet_node_0.wallet_state_manager.main_wallet + ph = await wallet_0.get_new_puzzlehash() + await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(full_node_server._port)), None) + + for i in range(3): + await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) + + all_blocks: List[FullBlock] = await full_node_api.get_all_full_blocks() + h: uint32 = all_blocks[-1].height + + await asyncio.sleep(3) + owner_sk: PrivateKey = master_sk_to_singleton_owner_sk(wsm.private_key, 3) + initial_state = PoolState(1, FARMING_TO_POOL, ph, owner_sk.get_g1(), "pool.com", uint32(10)) + tx_record, _, _ = await PoolWallet.create_new_pool_wallet_transaction(wsm, wallet_0, initial_state) + + launcher_spend: CoinSolution = tx_record.spend_bundle.coin_solutions[1] + + async with wsm.db_wrapper.lock: + pw = await PoolWallet.create( + wsm, wallet_0, launcher_spend.coin.name(), tx_record.spend_bundle.coin_solutions, h, True + ) + + log.warning(await pw.get_current_state()) + + # Claim rewards + # Escape pool + # Claim rewards + # Self pool diff --git a/tests/pools/test_wallet_pool_store.py b/tests/pools/test_wallet_pool_store.py new file mode 100644 index 000000000000..538fa1ffbb2c --- /dev/null +++ b/tests/pools/test_wallet_pool_store.py @@ -0,0 +1,135 @@ +import asyncio +import logging +from pathlib import Path +from secrets import token_bytes +from typing import Optional + +import aiosqlite +import pytest +from clvm_tools import binutils + +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.program import Program, SerializedProgram +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_solution import CoinSolution +from chia.util.db_wrapper import DBWrapper +from chia.util.ints import uint64 + +from chia.wallet.wallet_pool_store import WalletPoolStore + +log = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +def make_child_solution(coin_solution: CoinSolution, new_coin: Optional[Coin] = None) -> CoinSolution: + new_puzzle_hash: bytes32 = token_bytes(32) + solution = "()" + puzzle = f"(q . ((51 0x{new_puzzle_hash.hex()} 1)))" + puzzle_prog = Program.to(binutils.assemble(puzzle)) + solution_prog = Program.to(binutils.assemble(solution)) + if new_coin is None: + new_coin = coin_solution.additions()[0] + sol: CoinSolution = CoinSolution( + new_coin, + SerializedProgram.from_program(puzzle_prog), + SerializedProgram.from_program(solution_prog), + ) + log.warning("ABC") + log.warning(f"{sol.additions()}") + return sol + + +class TestWalletPoolStore: + @pytest.mark.asyncio + async def test_store(self): + db_filename = Path("wallet_store_test.db") + + if db_filename.exists(): + db_filename.unlink() + + db_connection = await aiosqlite.connect(db_filename) + db_wrapper = DBWrapper(db_connection) + store = await WalletPoolStore.create(db_wrapper) + try: + await db_wrapper.begin_transaction() + coin_0 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) + coin_0_alt = Coin(token_bytes(32), token_bytes(32), uint64(12312)) + solution_0: CoinSolution = make_child_solution(None, coin_0) + solution_0_alt: CoinSolution = make_child_solution(None, coin_0_alt) + solution_1: CoinSolution = make_child_solution(solution_0) + + assert store.get_spends_for_wallet(0) == [] + assert store.get_spends_for_wallet(1) == [] + + await store.add_spend(1, solution_1, 100) + assert store.get_spends_for_wallet(1) == [(100, solution_1)] + + # Idempotent + await store.add_spend(1, solution_1, 100) + assert store.get_spends_for_wallet(1) == [(100, solution_1)] + + with pytest.raises(ValueError): + await store.add_spend(1, solution_1, 101) + + # Rebuild cache, no longer present + await db_wrapper.rollback_transaction() + await store.rebuild_cache() + assert store.get_spends_for_wallet(1) == [] + + await store.rebuild_cache() + await store.add_spend(1, solution_1, 100) + assert store.get_spends_for_wallet(1) == [(100, solution_1)] + + solution_1_alt: CoinSolution = make_child_solution(solution_0_alt) + + with pytest.raises(ValueError): + await store.add_spend(1, solution_1_alt, 100) + + assert store.get_spends_for_wallet(1) == [(100, solution_1)] + + solution_2: CoinSolution = make_child_solution(solution_1) + await store.add_spend(1, solution_2, 100) + await store.rebuild_cache() + solution_3: CoinSolution = make_child_solution(solution_2) + await store.add_spend(1, solution_3, 100) + solution_4: CoinSolution = make_child_solution(solution_3) + + with pytest.raises(ValueError): + await store.add_spend(1, solution_4, 99) + + await store.rebuild_cache() + await store.add_spend(1, solution_4, 101) + await store.rebuild_cache() + await store.rollback(101, 1) + await store.rebuild_cache() + assert store.get_spends_for_wallet(1) == [ + (100, solution_1), + (100, solution_2), + (100, solution_3), + (101, solution_4), + ] + await store.rebuild_cache() + await store.rollback(100, 1) + await store.rebuild_cache() + assert store.get_spends_for_wallet(1) == [ + (100, solution_1), + (100, solution_2), + (100, solution_3), + ] + with pytest.raises(ValueError): + await store.add_spend(1, solution_1, 105) + + await store.add_spend(1, solution_4, 105) + solution_5: CoinSolution = make_child_solution(solution_4) + await store.add_spend(1, solution_5, 105) + await store.rollback(99, 1) + assert store.get_spends_for_wallet(1) == [] + + finally: + await db_connection.close() + db_filename.unlink() diff --git a/tests/runner-templates/build-test-macos b/tests/runner-templates/build-test-macos index 22414aba7fe8..383534dfc0d4 100644 --- a/tests/runner-templates/build-test-macos +++ b/tests/runner-templates/build-test-macos @@ -45,7 +45,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: # Note that new runners may break this https://github.com/actions/cache/issues/292 path: ${{ steps.pip-cache.outputs.dir }} diff --git a/tests/runner-templates/build-test-ubuntu b/tests/runner-templates/build-test-ubuntu index d59eb5f30d15..29a426ee8d48 100644 --- a/tests/runner-templates/build-test-ubuntu +++ b/tests/runner-templates/build-test-ubuntu @@ -40,7 +40,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache npm - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -53,7 +53,7 @@ jobs: echo "::set-output name=dir::$(pip cache dir)" - name: Cache pip - uses: actions/cache@v2.1.5 + uses: actions/cache@v2.1.6 with: path: ${{ steps.pip-cache.outputs.dir }} key: ${{ runner.os }}-pip-${{ hashFiles('**/setup.py') }} diff --git a/tests/runner-templates/checkout-test-plots.include.yml b/tests/runner-templates/checkout-test-plots.include.yml index f036a97a3d49..dcb198f1adcb 100644 --- a/tests/runner-templates/checkout-test-plots.include.yml +++ b/tests/runner-templates/checkout-test-plots.include.yml @@ -3,6 +3,6 @@ with: repository: 'Chia-Network/test-cache' path: '.chia' - ref: '0.26.0' + ref: '0.27.0' fetch-depth: 1 diff --git a/tests/testconfig.py b/tests/testconfig.py index 5a51d9c56a85..89874abab4f7 100644 --- a/tests/testconfig.py +++ b/tests/testconfig.py @@ -1,6 +1,6 @@ # Github actions template config. oses = ["ubuntu", "macos"] -root_test_dirs = ["blockchain", "clvm", "core", "generator", "simulation", "wallet"] +root_test_dirs = ["blockchain", "clvm", "core", "generator", "pools", "simulation", "wallet"] # Defaults are conservative. parallel = False diff --git a/tests/wallet/cc_wallet/test_cc_wallet.py b/tests/wallet/cc_wallet/test_cc_wallet.py index dcab54c11412..8f208ccf34e3 100644 --- a/tests/wallet/cc_wallet/test_cc_wallet.py +++ b/tests/wallet/cc_wallet/test_cc_wallet.py @@ -73,7 +73,7 @@ class TestCCWallet: await time_out_assert(15, wallet.get_confirmed_balance, funds) cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue() + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() tx_record = tx_queue[0] await time_out_assert( 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() @@ -113,7 +113,7 @@ class TestCCWallet: await time_out_assert(15, wallet.get_confirmed_balance, funds) cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue() + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() tx_record = tx_queue[0] await time_out_assert( 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() @@ -283,7 +283,7 @@ class TestCCWallet: await time_out_assert(15, wallet.get_confirmed_balance, funds) cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue() + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() tx_record = tx_queue[0] await time_out_assert( 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() @@ -363,7 +363,7 @@ class TestCCWallet: await time_out_assert(15, wallet_0.get_confirmed_balance, funds) cc_wallet_0: CCWallet = await CCWallet.create_new_cc(wallet_node_0.wallet_state_manager, wallet_0, uint64(100)) - tx_queue: List[TransactionRecord] = await wallet_node_0.wallet_state_manager.get_send_queue() + tx_queue: List[TransactionRecord] = await wallet_node_0.wallet_state_manager.tx_store.get_not_sent() tx_record = tx_queue[0] await time_out_assert( 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() @@ -461,7 +461,7 @@ class TestCCWallet: await time_out_assert(15, wallet.get_confirmed_balance, funds) cc_wallet: CCWallet = await CCWallet.create_new_cc(wallet_node.wallet_state_manager, wallet, uint64(100000)) - tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.get_send_queue() + tx_queue: List[TransactionRecord] = await wallet_node.wallet_state_manager.tx_store.get_not_sent() tx_record = tx_queue[0] await time_out_assert( 15, tx_in_pool, True, full_node_api.full_node.mempool_manager, tx_record.spend_bundle.name() diff --git a/tests/wallet/did_wallet/test_did.py b/tests/wallet/did_wallet/test_did.py index eeb4d001bf4a..f38fb64db15a 100644 --- a/tests/wallet/did_wallet/test_did.py +++ b/tests/wallet/did_wallet/test_did.py @@ -1,3 +1,4 @@ +""" import asyncio import time import pytest @@ -27,8 +28,7 @@ def event_loop(): loop = asyncio.get_event_loop() yield loop - -class TestDIDWallet: +class XTestDIDWallet: @pytest.fixture(scope="function") async def wallet_node(self): async for _ in setup_simulators_and_wallets(1, 1, {}): @@ -112,8 +112,7 @@ class TestDIDWallet: coins = await did_wallet_1.select_coins(1) coin = coins.copy().pop() assert did_wallet_2.did_info.temp_coin == coin - newpuz = await did_wallet_2.get_new_puzzle() - newpuzhash = newpuz.get_tree_hash() + newpuzhash = await did_wallet_2.get_new_inner_hash() pubkey = bytes( (await did_wallet_2.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id)).pubkey ) @@ -148,13 +147,15 @@ class TestDIDWallet: # DIDWallet3 spends the money back to itself ph2 = await wallet_1.get_new_puzzlehash() - await did_wallet_2.create_spend(ph2) + await did_wallet_2.create_exit_spend(ph2) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph)) - await time_out_assert(15, wallet_1.get_confirmed_balance, 201) - await time_out_assert(15, wallet_1.get_unconfirmed_balance, 201) + await time_out_assert(15, wallet_1.get_confirmed_balance, 200) + await time_out_assert(15, wallet_1.get_unconfirmed_balance, 200) + await time_out_assert(45, did_wallet_2.get_confirmed_balance, 0) + await time_out_assert(45, did_wallet_2.get_unconfirmed_balance, 0) @pytest.mark.asyncio async def test_did_recovery_with_multiple_backup_dids(self, two_wallet_nodes): @@ -224,29 +225,39 @@ class TestDIDWallet: await time_out_assert(15, did_wallet_3.get_unconfirmed_balance, 201) coins = await did_wallet_3.select_coins(1) coin = coins.pop() + + filename = "test.backup" + did_wallet_3.create_backup(filename) + + did_wallet_4 = await DIDWallet.create_new_did_wallet_from_recovery( + wallet_node.wallet_state_manager, + wallet, + filename, + ) pubkey = ( - await did_wallet_2.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id) + await did_wallet_4.wallet_state_manager.get_unused_derivation_record(did_wallet_2.wallet_info.id) ).pubkey - message_spend_bundle = await did_wallet.create_attestment(coin.name(), ph, pubkey, "test1.attest") - message_spend_bundle2 = await did_wallet_2.create_attestment(coin.name(), ph, pubkey, "test2.attest") + new_ph = await did_wallet_4.get_new_inner_hash() + message_spend_bundle = await did_wallet.create_attestment(coin.name(), new_ph, pubkey, "test1.attest") + message_spend_bundle2 = await did_wallet_2.create_attestment(coin.name(), new_ph, pubkey, "test2.attest") message_spend_bundle = message_spend_bundle.aggregate([message_spend_bundle, message_spend_bundle2]) ( test_info_list, test_message_spend_bundle, - ) = await did_wallet_3.load_attest_files_for_recovery_spend(["test1.attest", "test2.attest"]) + ) = await did_wallet_4.load_attest_files_for_recovery_spend(["test1.attest", "test2.attest"]) assert message_spend_bundle == test_message_spend_bundle for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) - await did_wallet_3.recovery_spend(coin, ph, test_info_list, pubkey, message_spend_bundle) + await did_wallet_4.recovery_spend(coin, new_ph, test_info_list, pubkey, message_spend_bundle) for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) - # ends in 899 so it got the 201 back - await time_out_assert(15, wallet2.get_confirmed_balance, 15999999999899) - await time_out_assert(15, wallet2.get_unconfirmed_balance, 15999999999899) + + await time_out_assert(15, did_wallet_4.get_confirmed_balance, 201) + await time_out_assert(15, did_wallet_4.get_unconfirmed_balance, 201) await time_out_assert(15, did_wallet_3.get_confirmed_balance, 0) await time_out_assert(15, did_wallet_3.get_unconfirmed_balance, 0) @@ -348,8 +359,7 @@ class TestDIDWallet: recovery_list = [bytes.fromhex(did_wallet_2.get_my_DID())] await did_wallet.update_recovery_list(recovery_list, uint64(1)) assert did_wallet.did_info.backup_ids == recovery_list - updated_puz = await did_wallet.get_new_puzzle() - await did_wallet.create_spend(updated_puz.get_tree_hash()) + await did_wallet.create_update_spend() for i in range(1, num_blocks): await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph2)) @@ -358,8 +368,7 @@ class TestDIDWallet: await time_out_assert(15, did_wallet.get_unconfirmed_balance, 101) # DID Wallet 2 recovers into itself with new innerpuz - new_puz = await did_wallet_2.get_new_puzzle() - new_ph = new_puz.get_tree_hash() + new_ph = await did_wallet_2.get_new_inner_hash() coins = await did_wallet_2.select_coins(1) coin = coins.pop() pubkey = ( @@ -620,3 +629,4 @@ class TestDIDWallet: # Assert coin ID is failing await time_out_assert(15, wallet.get_confirmed_balance, 23999999999899) await time_out_assert(15, wallet.get_unconfirmed_balance, 23999999999899) +""" diff --git a/tests/wallet/rpc/test_wallet_rpc.py b/tests/wallet/rpc/test_wallet_rpc.py index 7cbf2070f267..f9305d686d04 100644 --- a/tests/wallet/rpc/test_wallet_rpc.py +++ b/tests/wallet/rpc/test_wallet_rpc.py @@ -11,11 +11,10 @@ from chia.rpc.rpc_server import start_rpc_server from chia.rpc.wallet_rpc_api import WalletRpcApi from chia.rpc.wallet_rpc_client import WalletRpcClient from chia.simulator.simulator_protocol import FarmNewBlockProtocol -from chia.types.blockchain_format.coin import Coin from chia.types.peer_info import PeerInfo -from chia.types.spend_bundle import SpendBundle from chia.util.bech32m import encode_puzzle_hash from chia.util.ints import uint16, uint32 +from chia.wallet.transaction_record import TransactionRecord from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname from tests.time_out_assert import time_out_assert @@ -132,15 +131,16 @@ class TestWalletRpc: # Test basic transaction to one output signed_tx_amount = 888000 - tx_res = await client.create_signed_transaction([{"amount": signed_tx_amount, "puzzle_hash": ph_3}]) + tx_res: TransactionRecord = await client.create_signed_transaction( + [{"amount": signed_tx_amount, "puzzle_hash": ph_3}] + ) - assert tx_res["success"] - assert tx_res["signed_tx"]["fee_amount"] == 0 - assert tx_res["signed_tx"]["amount"] == signed_tx_amount - assert len(tx_res["signed_tx"]["additions"]) == 2 # The output and the change - assert any([addition["amount"] == signed_tx_amount for addition in tx_res["signed_tx"]["additions"]]) + assert tx_res.fee_amount == 0 + assert tx_res.amount == signed_tx_amount + assert len(tx_res.additions) == 2 # The output and the change + assert any([addition.amount == signed_tx_amount for addition in tx_res.additions]) - push_res = await client_node.push_tx(SpendBundle.from_json_dict(tx_res["signed_tx"]["spend_bundle"])) + push_res = await client_node.push_tx(tx_res.spend_bundle) assert push_res["success"] assert (await client.get_wallet_balance("1"))[ "confirmed_wallet_balance" @@ -154,9 +154,9 @@ class TestWalletRpc: # Test transaction to two outputs, from a specified coin, with a fee coin_to_spend = None - for addition in tx_res["signed_tx"]["additions"]: - if addition["amount"] != signed_tx_amount: - coin_to_spend = Coin.from_json_dict(addition) + for addition in tx_res.additions: + if addition.amount != signed_tx_amount: + coin_to_spend = addition assert coin_to_spend is not None tx_res = await client.create_signed_transaction( @@ -164,27 +164,43 @@ class TestWalletRpc: coins=[coin_to_spend], fee=100, ) - assert tx_res["success"] - assert tx_res["signed_tx"]["fee_amount"] == 100 - assert tx_res["signed_tx"]["amount"] == 444 + 999 - assert len(tx_res["signed_tx"]["additions"]) == 3 # The outputs and the change - assert any([addition["amount"] == 444 for addition in tx_res["signed_tx"]["additions"]]) - assert any([addition["amount"] == 999 for addition in tx_res["signed_tx"]["additions"]]) - assert ( - sum([rem["amount"] for rem in tx_res["signed_tx"]["removals"]]) - - sum([ad["amount"] for ad in tx_res["signed_tx"]["additions"]]) - == 100 - ) + assert tx_res.fee_amount == 100 + assert tx_res.amount == 444 + 999 + assert len(tx_res.additions) == 3 # The outputs and the change + assert any([addition.amount == 444 for addition in tx_res.additions]) + assert any([addition.amount == 999 for addition in tx_res.additions]) + assert sum([rem.amount for rem in tx_res.removals]) - sum([ad.amount for ad in tx_res.additions]) == 100 - push_res = await client_node.push_tx(SpendBundle.from_json_dict(tx_res["signed_tx"]["spend_bundle"])) + push_res = await client_node.push_tx(tx_res.spend_bundle) assert push_res["success"] for i in range(0, 5): await client.farm_block(encode_puzzle_hash(ph_2, "xch")) await asyncio.sleep(0.5) - await time_out_assert( - 5, eventual_balance, initial_funds_eventually - tx_amount - signed_tx_amount - 444 - 999 - 100 + new_balance = initial_funds_eventually - tx_amount - signed_tx_amount - 444 - 999 - 100 + await time_out_assert(5, eventual_balance, new_balance) + + send_tx_res: TransactionRecord = await client.send_transaction_multi( + "1", [{"amount": 555, "puzzle_hash": ph_4}, {"amount": 666, "puzzle_hash": ph_5}], fee=200 ) + assert send_tx_res is not None + assert send_tx_res.fee_amount == 200 + assert send_tx_res.amount == 555 + 666 + assert len(send_tx_res.additions) == 3 # The outputs and the change + assert any([addition.amount == 555 for addition in send_tx_res.additions]) + assert any([addition.amount == 666 for addition in send_tx_res.additions]) + assert ( + sum([rem.amount for rem in send_tx_res.removals]) - sum([ad.amount for ad in send_tx_res.additions]) + == 200 + ) + + await asyncio.sleep(3) + for i in range(0, 5): + await client.farm_block(encode_puzzle_hash(ph_2, "xch")) + await asyncio.sleep(0.5) + + new_balance = new_balance - 555 - 666 - 200 + await time_out_assert(5, eventual_balance, new_balance) address = await client.get_next_address("1", True) assert len(address) > 10 @@ -197,6 +213,17 @@ class TestWalletRpc: assert (await client.get_height_info()) > 0 + created_tx = await client.send_transaction("1", tx_amount, addr) + + async def tx_in_mempool_2(): + tx = await client.get_transaction("1", created_tx.name) + return tx.is_in_mempool() + + await time_out_assert(5, tx_in_mempool_2, True) + assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 1 + await client.delete_unconfirmed_transactions("1") + assert len(await wallet.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(1)) == 0 + sk_dict = await client.get_private_key(pks[0]) assert sk_dict["fingerprint"] == pks[0] assert sk_dict["sk"] is not None diff --git a/tests/wallet/test_singleton.py b/tests/wallet/test_singleton.py index e9dacbcd601f..bf3eba6f71a1 100644 --- a/tests/wallet/test_singleton.py +++ b/tests/wallet/test_singleton.py @@ -1,76 +1,126 @@ -from chia.wallet.puzzles.load_clvm import load_clvm -from chia.types.blockchain_format.program import Program, INFINITE_COST +from clvm_tools import binutils -DID_CORE_MOD = load_clvm("singleton_top_layer.clvm") +from chia.types.blockchain_format.program import Program, INFINITE_COST +from chia.types.announcement import Announcement +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.util.condition_tools import parse_sexp_to_conditions +from chia.wallet.puzzles.load_clvm import load_clvm + +SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") +LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") +P2_SINGLETON_MOD = load_clvm("p2_singleton.clvm") +POOL_MEMBER_MOD = load_clvm("pool_member_innerpuz.clvm") +POOL_WAITINGROOM_MOD = load_clvm("pool_waitingroom_innerpuz.clvm") + +LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() +SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() + +LAUNCHER_ID = Program.to(b"launcher-id").get_tree_hash() +POOL_REWARD_PREFIX_MAINNET = bytes32.fromhex("ccd5bb71183532bff220ba46c268991a00000000000000000000000000000000") + + +def singleton_puzzle(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> Program: + return SINGLETON_MOD.curry((SINGLETON_MOD_HASH, (launcher_id, launcher_puzzle_hash)), inner_puzzle) + + +def p2_singleton_puzzle(launcher_id: Program, launcher_puzzle_hash: bytes32) -> Program: + return P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, launcher_puzzle_hash) + + +def singleton_puzzle_hash(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> bytes32: + return singleton_puzzle(launcher_id, launcher_puzzle_hash, inner_puzzle).get_tree_hash() + + +def p2_singleton_puzzle_hash(launcher_id: Program, launcher_puzzle_hash: bytes32) -> bytes32: + return p2_singleton_puzzle(launcher_id, launcher_puzzle_hash).get_tree_hash() def test_only_odd_coins(): - did_core_hash = DID_CORE_MOD.get_tree_hash() + singleton_mod_hash = SINGLETON_MOD.get_tree_hash() + # (SINGLETON_STRUCT INNER_PUZZLE lineage_proof my_amount inner_solution) + # SINGLETON_STRUCT = (MOD_HASH . (LAUNCHER_ID . LAUNCHER_PUZZLE_HASH)) solution = Program.to( [ - did_core_hash, - did_core_hash, - 1, - [0xFADEDDAB, 203], + (singleton_mod_hash, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH)), + Program.to(binutils.assemble("(q (51 0xcafef00d 200))")), [0xDEADBEEF, 0xCAFEF00D, 200], 200, - [[51, 0xCAFEF00D, 200]], + [], ] ) try: - result, cost = DID_CORE_MOD.run_with_cost(INFINITE_COST, solution) + cost, result = SINGLETON_MOD.run_with_cost(INFINITE_COST, solution) except Exception as e: assert e.args == ("clvm raise",) else: assert False + solution = Program.to( [ - did_core_hash, - did_core_hash, - 1, - [0xFADEDDAB, 203], - [0xDEADBEEF, 0xCAFEF00D, 210], + (singleton_mod_hash, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH)), + Program.to(binutils.assemble("(q (51 0xcafef00d 201))")), + [0xDEADBEEF, 0xCAFED00D, 210], 205, - [[51, 0xCAFEF00D, 205]], + 0, ] ) try: - result, cost = DID_CORE_MOD.run_with_cost(INFINITE_COST, solution) + cost, result = SINGLETON_MOD.run_with_cost(INFINITE_COST, solution) except Exception: assert False def test_only_one_odd_coin_created(): - did_core_hash = DID_CORE_MOD.get_tree_hash() + singleton_mod_hash = SINGLETON_MOD.get_tree_hash() solution = Program.to( [ - did_core_hash, - did_core_hash, - 1, - [0xFADEDDAB, 203], + (singleton_mod_hash, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH)), + Program.to(binutils.assemble("(q (51 0xcafef00d 203) (51 0xfadeddab 205))")), [0xDEADBEEF, 0xCAFEF00D, 411], 411, - [[51, 0xCAFEF00D, 203], [51, 0xFADEDDAB, 203]], + [], ] ) try: - result, cost = DID_CORE_MOD.run_with_cost(INFINITE_COST, solution) + cost, result = SINGLETON_MOD.run_with_cost(INFINITE_COST, solution) except Exception as e: assert e.args == ("clvm raise",) else: assert False solution = Program.to( [ - did_core_hash, - did_core_hash, - 1, - [0xFADEDDAB, 203], + (singleton_mod_hash, (LAUNCHER_ID, LAUNCHER_PUZZLE_HASH)), + Program.to(binutils.assemble("(q (51 0xcafef00d 203) (51 0xfadeddab 204) (51 0xdeadbeef 202))")), [0xDEADBEEF, 0xCAFEF00D, 411], 411, - [[51, 0xCAFEF00D, 203], [51, 0xFADEDDAB, 202], [51, 0xFADEDDAB, 4]], + [], ] ) try: - result, cost = DID_CORE_MOD.run_with_cost(INFINITE_COST, solution) + cost, result = SINGLETON_MOD.run_with_cost(INFINITE_COST, solution) except Exception: assert False + + +def test_p2_singleton(): + # create a singleton. This should call driver code. + launcher_id = LAUNCHER_ID + innerpuz = Program.to(1) + singleton_full_puzzle = singleton_puzzle(launcher_id, LAUNCHER_PUZZLE_HASH, innerpuz) + + # create a fake coin id for the `p2_singleton` + p2_singleton_coin_id = Program.to(["test_hash"]).get_tree_hash() + expected_announcement = Announcement(singleton_full_puzzle.get_tree_hash(), p2_singleton_coin_id).name() + + # create a `p2_singleton` puzzle. This should call driver code. + p2_singleton_full = p2_singleton_puzzle(launcher_id, LAUNCHER_PUZZLE_HASH) + solution = Program.to([innerpuz.get_tree_hash(), p2_singleton_coin_id]) + cost, result = p2_singleton_full.run_with_cost(INFINITE_COST, solution) + err, conditions = parse_sexp_to_conditions(result) + assert err is None + + p2_singleton_full = p2_singleton_puzzle(launcher_id, LAUNCHER_PUZZLE_HASH) + solution = Program.to([innerpuz.get_tree_hash(), p2_singleton_coin_id]) + cost, result = p2_singleton_full.run_with_cost(INFINITE_COST, solution) + assert result.first().rest().first().as_atom() == expected_announcement + assert conditions[0].vars[0] == expected_announcement diff --git a/tests/wallet/test_singleton_lifecycle.py b/tests/wallet/test_singleton_lifecycle.py new file mode 100644 index 000000000000..0cac271a9916 --- /dev/null +++ b/tests/wallet/test_singleton_lifecycle.py @@ -0,0 +1,146 @@ +import asyncio + +from typing import List, Tuple + +from blspy import G2Element +from clvm_tools import binutils + +from chia.types.blockchain_format.program import Program, INFINITE_COST +from chia.types.announcement import Announcement +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_solution import CoinSolution +from chia.types.spend_bundle import SpendBundle +from chia.util.condition_tools import ConditionOpcode + +from chia.util.ints import uint64 +from chia.wallet.puzzles.load_clvm import load_clvm + +from tests.core.full_node.test_conditions import bt, check_spend_bundle_validity, initial_blocks + + +SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") +LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") +P2_SINGLETON_MOD = load_clvm("p2_singleton.clvm") +POOL_MEMBER_MOD = load_clvm("pool_member_innerpuz.clvm") +POOL_WAITINGROOM_MOD = load_clvm("pool_waitingroom_innerpuz.clvm") + +LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() +SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() + +POOL_REWARD_PREFIX_MAINNET = bytes32.fromhex("ccd5bb71183532bff220ba46c268991a00000000000000000000000000000000") + + +def check_coin_solution(coin_solution: CoinSolution): + # breakpoint() + try: + cost, result = coin_solution.puzzle_reveal.run_with_cost(INFINITE_COST, coin_solution.solution) + except Exception as ex: + print(ex) + # breakpoint() + print(ex) + + +def adaptor_for_singleton_inner_puzzle(puzzle: Program) -> Program: + # this is prety slow + return Program.to(binutils.assemble("(a (q . %s) 3)" % binutils.disassemble(puzzle))) + + +def launcher_conditions_and_spend_bundle( + parent_coin_id: bytes32, + launcher_amount: uint64, + initial_singleton_inner_puzzle: Program, + metadata: List[Tuple[str, str]], + launcher_puzzle: Program = LAUNCHER_PUZZLE, +) -> Tuple[Program, bytes32, List[Program], SpendBundle]: + launcher_puzzle_hash = launcher_puzzle.get_tree_hash() + launcher_coin = Coin(parent_coin_id, launcher_puzzle_hash, launcher_amount) + singleton_full_puzzle = SINGLETON_MOD.curry( + SINGLETON_MOD_HASH, launcher_coin.name(), launcher_puzzle_hash, initial_singleton_inner_puzzle + ) + singleton_full_puzzle_hash = singleton_full_puzzle.get_tree_hash() + message_program = Program.to([singleton_full_puzzle_hash, launcher_amount, metadata]) + expected_announcement = Announcement(launcher_coin.name(), message_program.get_tree_hash()) + expected_conditions = [] + expected_conditions.append( + Program.to( + binutils.assemble(f"(0x{ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT.hex()} 0x{expected_announcement.name()})") + ) + ) + expected_conditions.append( + Program.to( + binutils.assemble(f"(0x{ConditionOpcode.CREATE_COIN.hex()} 0x{launcher_puzzle_hash} {launcher_amount})") + ) + ) + launcher_solution = Program.to([singleton_full_puzzle_hash, launcher_amount, metadata]) + coin_solution = CoinSolution(launcher_coin, launcher_puzzle, launcher_solution) + spend_bundle = SpendBundle([coin_solution], G2Element()) + lineage_proof = Program.to([parent_coin_id, launcher_amount]) + return lineage_proof, launcher_coin.name(), expected_conditions, spend_bundle + + +def singleton_puzzle(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> Program: + return SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, launcher_puzzle_hash, inner_puzzle) + + +def singleton_puzzle_hash(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> bytes32: + return singleton_puzzle(launcher_id, launcher_puzzle_hash, inner_puzzle).get_tree_hash() + + +def solution_for_singleton_puzzle(lineage_proof: Program, my_amount: int, inner_solution: Program) -> Program: + return Program.to([lineage_proof, my_amount, inner_solution]) + + +def p2_singleton_puzzle(launcher_id: Program, launcher_puzzle_hash: bytes32) -> Program: + return P2_SINGLETON_MOD.curry(SINGLETON_MOD_HASH, launcher_id, launcher_puzzle_hash) + + +def p2_singleton_puzzle_hash(launcher_id: Program, launcher_puzzle_hash: bytes32) -> bytes32: + return p2_singleton_puzzle(launcher_id, launcher_puzzle_hash).get_tree_hash() + + +def test_only_odd_coins_0(): + blocks = initial_blocks() + farmed_coin = list(blocks[-1].get_included_reward_coins())[0] + + metadata = [("foo", "bar")] + ANYONE_CAN_SPEND_PUZZLE = Program.to(1) + launcher_amount = uint64(1) + launcher_puzzle = LAUNCHER_PUZZLE + launcher_puzzle_hash = launcher_puzzle.get_tree_hash() + initial_singleton_puzzle = adaptor_for_singleton_inner_puzzle(ANYONE_CAN_SPEND_PUZZLE) + lineage_proof, launcher_id, condition_list, launcher_spend_bundle = launcher_conditions_and_spend_bundle( + farmed_coin.name(), launcher_amount, initial_singleton_puzzle, metadata, launcher_puzzle + ) + + conditions = Program.to(condition_list) + coin_solution = CoinSolution(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, conditions) + spend_bundle = SpendBundle.aggregate([launcher_spend_bundle, SpendBundle([coin_solution], G2Element())]) + run = asyncio.get_event_loop().run_until_complete + coins_added, coins_removed = run(check_spend_bundle_validity(bt.constants, blocks, spend_bundle)) + + coin_set_added = set([_.coin for _ in coins_added]) + coin_set_removed = set([_.coin for _ in coins_removed]) + + launcher_coin = launcher_spend_bundle.coin_solutions[0].coin + + assert launcher_coin in coin_set_added + assert launcher_coin in coin_set_removed + + assert farmed_coin in coin_set_removed + # breakpoint() + + singleton_expected_puzzle_hash = singleton_puzzle_hash(launcher_id, launcher_puzzle_hash, initial_singleton_puzzle) + expected_singleton_coin = Coin(launcher_coin.name(), singleton_expected_puzzle_hash, launcher_amount) + assert expected_singleton_coin in coin_set_added + + # next up: spend the expected_singleton_coin + # it's an adapted `ANYONE_CAN_SPEND_PUZZLE` + + # then try a bad lineage proof + # then try writing two odd coins + # then try writing zero odd coins + + # then, destroy the singleton with the -113 hack + + return 0 diff --git a/tests/wallet/test_singleton_lifecycle_fast.py b/tests/wallet/test_singleton_lifecycle_fast.py new file mode 100644 index 000000000000..82e21f2d52c3 --- /dev/null +++ b/tests/wallet/test_singleton_lifecycle_fast.py @@ -0,0 +1,749 @@ +from dataclasses import dataclass +from typing import Any, Callable, Dict, List, Optional, Tuple + +from blspy import G1Element, G2Element +from clvm_tools import binutils + +from chia.types.blockchain_format.program import Program, SerializedProgram +from chia.types.announcement import Announcement +from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_solution import CoinSolution as CoinSpend +from chia.types.spend_bundle import SpendBundle +from chia.util.condition_tools import ConditionOpcode +from chia.util.ints import uint64 +from chia.wallet.puzzles.load_clvm import load_clvm + +from tests.clvm.coin_store import BadSpendBundleError, CoinStore, CoinTimestamp + + +SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") +LAUNCHER_PUZZLE = load_clvm("singleton_launcher.clvm") +P2_SINGLETON_MOD = load_clvm("p2_singleton_or_delayed_puzhash.clvm") +POOL_MEMBER_MOD = load_clvm("pool_member_innerpuz.clvm") +POOL_WAITINGROOM_MOD = load_clvm("pool_waitingroom_innerpuz.clvm") + +LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() +SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() +P2_SINGLETON_MOD_HASH = P2_SINGLETON_MOD.get_tree_hash() + +ANYONE_CAN_SPEND_PUZZLE = Program.to(1) +ANYONE_CAN_SPEND_WITH_PADDING_PUZZLE_HASH = Program.to(binutils.assemble("(a (q . 1) 3)")).get_tree_hash() + +POOL_REWARD_PREFIX_MAINNET = bytes32.fromhex("ccd5bb71183532bff220ba46c268991a00000000000000000000000000000000") + +MAX_BLOCK_COST_CLVM = int(1e18) + + +class PuzzleDB: + def __init__(self): + self._db = {} + + def add_puzzle(self, puzzle: Program): + self._db[puzzle.get_tree_hash()] = Program.from_bytes(bytes(puzzle)) + + def puzzle_for_hash(self, puzzle_hash: bytes32) -> Optional[Program]: + return self._db.get(puzzle_hash) + + +def from_kwargs(kwargs, key, type_info=Any): + """Raise an exception if `kwargs[key]` is missing or the wrong type""" + """for now, we just check that it's present""" + if key not in kwargs: + raise ValueError(f"`{key}` missing in call to `solve`") + return kwargs[key] + + +Solver_F = Callable[["Solver", PuzzleDB, List[Program], Any], Program] + + +class Solver: + """ + This class registers puzzle templates by hash and solves them. + """ + + def __init__(self): + self.solvers_by_puzzle_hash = {} + + def register_solver(self, puzzle_hash: bytes32, solver_f: Solver_F): + if puzzle_hash in self.solvers_by_puzzle_hash: + raise ValueError(f"solver registered for {puzzle_hash}") + self.solvers_by_puzzle_hash[puzzle_hash] = solver_f + + def solve(self, puzzle_db: PuzzleDB, puzzle: Program, **kwargs: Any) -> Program: + """ + The legal values and types for `kwargs` depends on the underlying solver + that's invoked. The `kwargs` are passed through to any inner solvers + that may need to be called. + """ + puzzle_hash = puzzle.get_tree_hash() + puzzle_args = [] + if puzzle_hash not in self.solvers_by_puzzle_hash: + puzzle_template, args = puzzle.uncurry() + puzzle_args = list(args.as_iter()) + puzzle_hash = puzzle_template.get_tree_hash() + solver_f = self.solvers_by_puzzle_hash.get(puzzle_hash) + if solver_f: + return solver_f(self, puzzle_db, puzzle_args, kwargs) + + raise ValueError("can't solve") + + +def solve_launcher(solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict) -> Program: + launcher_amount = from_kwargs(kwargs, "launcher_amount", int) + destination_puzzle_hash = from_kwargs(kwargs, "destination_puzzle_hash", bytes32) + metadata = from_kwargs(kwargs, "metadata", List[Tuple[str, Program]]) + solution = Program.to([destination_puzzle_hash, launcher_amount, metadata]) + return solution + + +def solve_anyone_can_spend(solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict) -> Program: + """ + This is the anyone-can-spend puzzle `1`. Note that farmers can easily steal this coin, so don't use + it except for testing. + """ + conditions = from_kwargs(kwargs, "conditions", List[Program]) + solution = Program.to(conditions) + return solution + + +def solve_anyone_can_spend_with_padding( + solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict +) -> Program: + """This is the puzzle `(a (q . 1) 3)`. It's only for testing.""" + conditions = from_kwargs(kwargs, "conditions", List[Program]) + solution = Program.to((0, conditions)) + return solution + + +def solve_singleton(solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict) -> Program: + """ + `lineage_proof`: a `Program` that proves the parent is also a singleton (or the launcher). + `coin_amount`: a necessarily-odd value of mojos in this coin. + """ + singleton_struct, inner_puzzle = args + inner_solution = solver.solve(puzzle_db, inner_puzzle, **kwargs) + lineage_proof = from_kwargs(kwargs, "lineage_proof", Program) + coin_amount = from_kwargs(kwargs, "coin_amount", int) + solution = inner_solution.to([lineage_proof, coin_amount, inner_solution.rest()]) + return solution + + +def solve_pool_member(solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict) -> Program: + pool_member_spend_type = from_kwargs(kwargs, "pool_member_spend_type") + allowable = ["to-waiting-room", "claim-p2-nft"] + if pool_member_spend_type not in allowable: + raise ValueError("`pool_member_spend_type` must be one of %s for POOL_MEMBER puzzle" % "/".join(allowable)) + to_waiting_room = pool_member_spend_type == "to-waiting-room" + if to_waiting_room: + key_value_list = from_kwargs(kwargs, "key_value_list", List[Tuple[str, Program]]) + return Program.to([0, 1, 0, 0, key_value_list]) + # it's an "absorb_pool_reward" type + pool_reward_amount = from_kwargs(kwargs, "pool_reward_amount", int) + pool_reward_height = from_kwargs(kwargs, "pool_reward_height", int) + solution = Program.to([0, pool_reward_amount, pool_reward_height]) + return solution + + +def solve_pool_waiting_room(solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict) -> Program: + pool_leaving_spend_type = from_kwargs(kwargs, "pool_leaving_spend_type") + allowable = ["exit-waiting-room", "claim-p2-nft"] + if pool_leaving_spend_type not in allowable: + raise ValueError("`pool_leaving_spend_type` must be one of %s for POOL_MEMBER puzzle" % "/".join(allowable)) + exit_waiting_room = pool_leaving_spend_type == "exit-waiting-room" + if exit_waiting_room: + key_value_list = from_kwargs(kwargs, "key_value_list", List[Tuple[str, Program]]) + destination_puzzle_hash = from_kwargs(kwargs, "destination_puzzle_hash", int) + return Program.to([0, 1, key_value_list, destination_puzzle_hash]) + # it's an "absorb_pool_reward" type + pool_reward_amount = from_kwargs(kwargs, "pool_reward_amount", int) + pool_reward_height = from_kwargs(kwargs, "pool_reward_height", int) + solution = Program.to([0, 0, pool_reward_amount, pool_reward_height]) + return solution + + +def solve_p2_singleton(solver: Solver, puzzle_db: PuzzleDB, args: List[Program], kwargs: Dict) -> Program: + p2_singleton_spend_type = from_kwargs(kwargs, "p2_singleton_spend_type") + allowable = ["claim-p2-nft", "delayed-spend"] + if p2_singleton_spend_type not in allowable: + raise ValueError("`p2_singleton_spend_type` must be one of %s for P2_SINGLETON puzzle" % "/".join(allowable)) + claim_p2_nft = p2_singleton_spend_type == "claim-p2-nft" + if claim_p2_nft: + singleton_inner_puzzle_hash = from_kwargs(kwargs, "singleton_inner_puzzle_hash") + p2_singleton_coin_name = from_kwargs(kwargs, "p2_singleton_coin_name") + solution = Program.to([singleton_inner_puzzle_hash, p2_singleton_coin_name]) + return solution + raise ValueError("can't solve `delayed-spend` yet") + + +SOLVER = Solver() +SOLVER.register_solver(LAUNCHER_PUZZLE_HASH, solve_launcher) +SOLVER.register_solver(ANYONE_CAN_SPEND_WITH_PADDING_PUZZLE_HASH, solve_anyone_can_spend_with_padding) +SOLVER.register_solver(SINGLETON_MOD_HASH, solve_singleton) +SOLVER.register_solver(POOL_MEMBER_MOD.get_tree_hash(), solve_pool_member) +SOLVER.register_solver(POOL_WAITINGROOM_MOD.get_tree_hash(), solve_pool_waiting_room) +SOLVER.register_solver(ANYONE_CAN_SPEND_PUZZLE.get_tree_hash(), solve_anyone_can_spend) +SOLVER.register_solver(P2_SINGLETON_MOD_HASH, solve_p2_singleton) + + +def solve_puzzle(puzzle_db: PuzzleDB, puzzle: Program, **kwargs) -> Program: + return SOLVER.solve(puzzle_db, puzzle, **kwargs) + + +@dataclass +class SingletonWallet: + launcher_id: bytes32 + launcher_puzzle_hash: bytes32 + key_value_list: Program + current_state: Coin + lineage_proof: Program + + def inner_puzzle(self, puzzle_db: PuzzleDB) -> Optional[Program]: + puzzle = puzzle_db.puzzle_for_hash(self.current_state.puzzle_hash) + if puzzle is None: + return None + return self.inner_puzzle_for_puzzle(puzzle) + + def inner_puzzle_for_puzzle(self, puzzle: Program) -> Optional[Program]: + assert puzzle.get_tree_hash() == self.current_state.puzzle_hash + if puzzle is None: + return puzzle + template, args = puzzle.uncurry() + assert bytes(template) == bytes(SINGLETON_MOD) + singleton_struct, inner_puzzle = list(args.as_iter()) + return inner_puzzle + + def coin_spend_for_conditions(self, puzzle_db: PuzzleDB, **kwargs) -> CoinSpend: + coin = self.current_state + puzzle_reveal = puzzle_db.puzzle_for_hash(coin.puzzle_hash) + assert puzzle_reveal is not None + solution = solve_puzzle( + puzzle_db, puzzle_reveal, lineage_proof=self.lineage_proof, coin_amount=coin.amount, **kwargs + ) + return CoinSpend(coin, puzzle_reveal, solution) + + def update_state(self, puzzle_db: PuzzleDB, removals: List[CoinSpend]) -> int: + state_change_count = 0 + current_coin_name = self.current_state.name() + for coin_spend in removals: + if coin_spend.coin.name() == current_coin_name: + for coin in coin_spend.additions(): + if coin.amount & 1 == 1: + parent_puzzle_hash = coin_spend.coin.puzzle_hash + parent_puzzle = puzzle_db.puzzle_for_hash(parent_puzzle_hash) + assert parent_puzzle is not None + parent_inner_puzzle = self.inner_puzzle_for_puzzle(parent_puzzle) + assert parent_inner_puzzle is not None + parent_inner_puzzle_hash = parent_inner_puzzle.get_tree_hash() + lineage_proof = Program.to( + [self.current_state.parent_coin_info, parent_inner_puzzle_hash, coin.amount] + ) + self.lineage_proof = lineage_proof + self.current_state = coin + state_change_count += 1 + return state_change_count + + +def adaptor_for_singleton_inner_puzzle(puzzle: Program) -> Program: + """ + The singleton puzzle requires an inner puzzle which gets passed some "truths" from + the singleton that are guaranteed to be correct. Using these truths may reduce the + size of the inner puzzle, since any values can be used knowing they are checked elsewhere. + However, an inner puzzle that is not aware that this first argument contains these + values can be "adapted" using this function to ignore the first argument (and slide + the subsequent arguments over), allowing any inner puzzle that thinks it's an outer + puzzle to work as a singleton inner puzzle. + """ + # this is pretty slow and lame + return Program.to(binutils.assemble("(a (q . %s) 3)" % binutils.disassemble(puzzle))) + + +def launcher_conditions_and_spend_bundle( + puzzle_db: PuzzleDB, + parent_coin_id: bytes32, + launcher_amount: uint64, + initial_singleton_inner_puzzle: Program, + metadata: List[Tuple[str, str]], + launcher_puzzle: Program, +) -> Tuple[bytes32, List[Program], SpendBundle]: + puzzle_db.add_puzzle(launcher_puzzle) + launcher_puzzle_hash = launcher_puzzle.get_tree_hash() + launcher_coin = Coin(parent_coin_id, launcher_puzzle_hash, launcher_amount) + singleton_full_puzzle = singleton_puzzle(launcher_coin.name(), launcher_puzzle_hash, initial_singleton_inner_puzzle) + puzzle_db.add_puzzle(singleton_full_puzzle) + singleton_full_puzzle_hash = singleton_full_puzzle.get_tree_hash() + message_program = Program.to([singleton_full_puzzle_hash, launcher_amount, metadata]) + expected_announcement = Announcement(launcher_coin.name(), message_program.get_tree_hash()) + expected_conditions = [] + expected_conditions.append( + Program.to( + binutils.assemble(f"(0x{ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT.hex()} 0x{expected_announcement.name()})") + ) + ) + expected_conditions.append( + Program.to( + binutils.assemble(f"(0x{ConditionOpcode.CREATE_COIN.hex()} 0x{launcher_puzzle_hash} {launcher_amount})") + ) + ) + solution = solve_puzzle( + puzzle_db, + launcher_puzzle, + destination_puzzle_hash=singleton_full_puzzle_hash, + launcher_amount=launcher_amount, + metadata=metadata, + ) + coin_spend = CoinSpend(launcher_coin, SerializedProgram.from_program(launcher_puzzle), solution) + spend_bundle = SpendBundle([coin_spend], G2Element()) + return launcher_coin.name(), expected_conditions, spend_bundle + + +def singleton_puzzle(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> Program: + return SINGLETON_MOD.curry((SINGLETON_MOD_HASH, (launcher_id, launcher_puzzle_hash)), inner_puzzle) + + +def singleton_puzzle_hash(launcher_id: Program, launcher_puzzle_hash: bytes32, inner_puzzle: Program) -> bytes32: + return singleton_puzzle(launcher_id, launcher_puzzle_hash, inner_puzzle).get_tree_hash() + + +def solution_for_singleton_puzzle(lineage_proof: Program, my_amount: int, inner_solution: Program) -> Program: + return Program.to([lineage_proof, my_amount, inner_solution]) + + +def p2_singleton_puzzle_for_launcher( + puzzle_db: PuzzleDB, + launcher_id: Program, + launcher_puzzle_hash: bytes32, + seconds_delay: int, + delayed_puzzle_hash: bytes32, +) -> Program: + puzzle = P2_SINGLETON_MOD.curry( + SINGLETON_MOD_HASH, launcher_id, launcher_puzzle_hash, seconds_delay, delayed_puzzle_hash + ) + puzzle_db.add_puzzle(puzzle) + return puzzle + + +def p2_singleton_puzzle_hash_for_launcher( + puzzle_db: PuzzleDB, + launcher_id: Program, + launcher_puzzle_hash: bytes32, + seconds_delay: int, + delayed_puzzle_hash: bytes32, +) -> bytes32: + return p2_singleton_puzzle_for_launcher( + puzzle_db, launcher_id, launcher_puzzle_hash, seconds_delay, delayed_puzzle_hash + ).get_tree_hash() + + +def claim_p2_singleton( + puzzle_db: PuzzleDB, singleton_wallet: SingletonWallet, p2_singleton_coin: Coin +) -> Tuple[CoinSpend, List[Program]]: + inner_puzzle = singleton_wallet.inner_puzzle(puzzle_db) + assert inner_puzzle + inner_puzzle_hash = inner_puzzle.get_tree_hash() + p2_singleton_puzzle = puzzle_db.puzzle_for_hash(p2_singleton_coin.puzzle_hash) + assert p2_singleton_puzzle is not None + p2_singleton_coin_name = p2_singleton_coin.name() + p2_singleton_solution = solve_puzzle( + puzzle_db, + p2_singleton_puzzle, + p2_singleton_spend_type="claim-p2-nft", + singleton_inner_puzzle_hash=inner_puzzle_hash, + p2_singleton_coin_name=p2_singleton_coin_name, + ) + p2_singleton_coin_spend = CoinSpend( + p2_singleton_coin, + p2_singleton_puzzle.to_serialized_program(), + p2_singleton_solution, + ) + expected_p2_singleton_announcement = Announcement(p2_singleton_coin_name, bytes(b"$")).name() + singleton_conditions = [ + Program.to([ConditionOpcode.CREATE_PUZZLE_ANNOUNCEMENT, p2_singleton_coin_name]), + Program.to([ConditionOpcode.CREATE_COIN, inner_puzzle_hash, 1]), + Program.to([ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, expected_p2_singleton_announcement]), + ] + return p2_singleton_coin_spend, singleton_conditions + + +def lineage_proof_for_coin_spend(coin_spend: CoinSpend) -> Program: + """Take a coin solution, return a lineage proof for their child to use in spends""" + coin = coin_spend.coin + parent_name = coin.parent_coin_info + amount = coin.amount + + inner_puzzle_hash = None + if coin.puzzle_hash == LAUNCHER_PUZZLE_HASH: + return Program.to([parent_name, amount]) + + full_puzzle = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) + _, args = full_puzzle.uncurry() + _, __, ___, inner_puzzle = list(args.as_iter()) + inner_puzzle_hash = inner_puzzle.get_tree_hash() + + return Program.to([parent_name, inner_puzzle_hash, amount]) + + +def create_throwaway_pubkey(seed: bytes) -> G1Element: + return G1Element.generator() + + +def assert_coin_spent(coin_store: CoinStore, coin: Coin, is_spent=True): + coin_record = coin_store.coin_record(coin.name()) + assert coin_record is not None + assert coin_record.spent is is_spent + + +def spend_coin_to_singleton( + puzzle_db: PuzzleDB, launcher_puzzle: Program, coin_store: CoinStore, now: CoinTimestamp +) -> Tuple[List[Coin], List[CoinSpend]]: + + farmed_coin_amount = 100000 + metadata = [("foo", "bar")] + + now = CoinTimestamp(10012300, 1) + farmed_coin = coin_store.farm_coin(ANYONE_CAN_SPEND_PUZZLE.get_tree_hash(), now, amount=farmed_coin_amount) + now.seconds += 500 + now.height += 1 + + launcher_amount: uint64 = uint64(1) + launcher_puzzle = LAUNCHER_PUZZLE + launcher_puzzle_hash = launcher_puzzle.get_tree_hash() + initial_singleton_puzzle = adaptor_for_singleton_inner_puzzle(ANYONE_CAN_SPEND_PUZZLE) + launcher_id, condition_list, launcher_spend_bundle = launcher_conditions_and_spend_bundle( + puzzle_db, farmed_coin.name(), launcher_amount, initial_singleton_puzzle, metadata, launcher_puzzle + ) + + conditions = Program.to(condition_list) + coin_spend = CoinSpend(farmed_coin, ANYONE_CAN_SPEND_PUZZLE, conditions) + spend_bundle = SpendBundle.aggregate([launcher_spend_bundle, SpendBundle([coin_spend], G2Element())]) + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + + launcher_coin = launcher_spend_bundle.coin_solutions[0].coin + + assert_coin_spent(coin_store, launcher_coin) + assert_coin_spent(coin_store, farmed_coin) + + singleton_expected_puzzle = singleton_puzzle(launcher_id, launcher_puzzle_hash, initial_singleton_puzzle) + singleton_expected_puzzle_hash = singleton_expected_puzzle.get_tree_hash() + expected_singleton_coin = Coin(launcher_coin.name(), singleton_expected_puzzle_hash, launcher_amount) + assert_coin_spent(coin_store, expected_singleton_coin, is_spent=False) + + return additions, removals + + +def find_interesting_singletons(puzzle_db: PuzzleDB, removals: List[CoinSpend]) -> List[SingletonWallet]: + singletons = [] + for coin_spend in removals: + if coin_spend.coin.puzzle_hash == LAUNCHER_PUZZLE_HASH: + r = Program.from_bytes(bytes(coin_spend.solution)) + key_value_list = r.rest().rest().first() + + eve_coin = coin_spend.additions()[0] + + lineage_proof = lineage_proof_for_coin_spend(coin_spend) + launcher_id = coin_spend.coin.name() + singleton = SingletonWallet( + launcher_id, + coin_spend.coin.puzzle_hash, + key_value_list, + eve_coin, + lineage_proof, + ) + singletons.append(singleton) + return singletons + + +def filter_p2_singleton(puzzle_db: PuzzleDB, singleton_wallet: SingletonWallet, additions: List[Coin]) -> List[Coin]: + r = [] + for coin in additions: + puzzle = puzzle_db.puzzle_for_hash(coin.puzzle_hash) + if puzzle is None: + continue + template, args = puzzle.uncurry() + if template.get_tree_hash() == P2_SINGLETON_MOD_HASH: + r.append(coin) + return r + + +def test_lifecycle_with_coinstore_as_wallet(): + + PUZZLE_DB = PuzzleDB() + + interested_singletons = [] + + ####### + # farm a coin + + coin_store = CoinStore(int.from_bytes(POOL_REWARD_PREFIX_MAINNET, "big")) + now = CoinTimestamp(10012300, 1) + + DELAY_SECONDS = 86400 + DELAY_PUZZLE_HASH = bytes([0] * 32) + + ####### + # spend coin to a singleton + + additions, removals = spend_coin_to_singleton(PUZZLE_DB, LAUNCHER_PUZZLE, coin_store, now) + + assert len(list(coin_store.all_unspent_coins())) == 1 + + new_singletons = find_interesting_singletons(PUZZLE_DB, removals) + interested_singletons.extend(new_singletons) + + assert len(interested_singletons) == 1 + + SINGLETON_WALLET = interested_singletons[0] + + ####### + # farm a `p2_singleton` + + pool_reward_puzzle_hash = p2_singleton_puzzle_hash_for_launcher( + PUZZLE_DB, SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, DELAY_SECONDS, DELAY_PUZZLE_HASH + ) + farmed_coin = coin_store.farm_coin(pool_reward_puzzle_hash, now) + now.seconds += 500 + now.height += 1 + + p2_singleton_coins = filter_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, [farmed_coin]) + assert p2_singleton_coins == [farmed_coin] + + assert len(list(coin_store.all_unspent_coins())) == 2 + + ####### + # now collect the `p2_singleton` using the singleton + + for coin in p2_singleton_coins: + p2_singleton_coin_spend, singleton_conditions = claim_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, coin) + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions(PUZZLE_DB, conditions=singleton_conditions) + spend_bundle = SpendBundle([coin_spend, p2_singleton_coin_spend], G2Element()) + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + now.seconds += 500 + now.height += 1 + + SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + assert len(list(coin_store.all_unspent_coins())) == 1 + + ####### + # farm and collect another `p2_singleton` + + pool_reward_puzzle_hash = p2_singleton_puzzle_hash_for_launcher( + PUZZLE_DB, SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, DELAY_SECONDS, DELAY_PUZZLE_HASH + ) + farmed_coin = coin_store.farm_coin(pool_reward_puzzle_hash, now) + now.seconds += 500 + now.height += 1 + + p2_singleton_coins = filter_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, [farmed_coin]) + assert p2_singleton_coins == [farmed_coin] + + assert len(list(coin_store.all_unspent_coins())) == 2 + + for coin in p2_singleton_coins: + p2_singleton_coin_spend, singleton_conditions = claim_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, coin) + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions(PUZZLE_DB, conditions=singleton_conditions) + spend_bundle = SpendBundle([coin_spend, p2_singleton_coin_spend], G2Element()) + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + now.seconds += 500 + now.height += 1 + + SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + assert len(list(coin_store.all_unspent_coins())) == 1 + + ####### + # loan the singleton to a pool + + # puzzle_for_loan_singleton_to_pool( + # pool_puzzle_hash, p2_singleton_puzzle_hash, owner_public_key, pool_reward_prefix, relative_lock_height) + + # calculate the series + + owner_public_key = bytes(create_throwaway_pubkey(b"foo")) + pool_puzzle_hash = Program.to(bytes(create_throwaway_pubkey(b""))).get_tree_hash() + pool_reward_prefix = POOL_REWARD_PREFIX_MAINNET + relative_lock_height = 1440 + + pool_escaping_puzzle = POOL_WAITINGROOM_MOD.curry( + pool_puzzle_hash, pool_reward_puzzle_hash, owner_public_key, pool_reward_prefix, relative_lock_height + ) + pool_escaping_puzzle_hash = pool_escaping_puzzle.get_tree_hash() + + pool_member_puzzle = POOL_MEMBER_MOD.curry( + pool_puzzle_hash, + pool_reward_puzzle_hash, + owner_public_key, + pool_reward_prefix, + pool_escaping_puzzle_hash, + ) + pool_member_puzzle_hash = pool_member_puzzle.get_tree_hash() + + PUZZLE_DB.add_puzzle(pool_escaping_puzzle) + PUZZLE_DB.add_puzzle( + singleton_puzzle(SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, pool_escaping_puzzle) + ) + PUZZLE_DB.add_puzzle(pool_member_puzzle) + full_puzzle = singleton_puzzle( + SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, pool_member_puzzle + ) + PUZZLE_DB.add_puzzle(full_puzzle) + + conditions = [Program.to([ConditionOpcode.CREATE_COIN, pool_member_puzzle_hash, 1])] + + singleton_coin_spend = SINGLETON_WALLET.coin_spend_for_conditions(PUZZLE_DB, conditions=conditions) + + spend_bundle = SpendBundle([singleton_coin_spend], G2Element()) + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + + assert len(list(coin_store.all_unspent_coins())) == 1 + + SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + ####### + # farm a `p2_singleton` + + pool_reward_puzzle_hash = p2_singleton_puzzle_hash_for_launcher( + PUZZLE_DB, SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, DELAY_SECONDS, DELAY_PUZZLE_HASH + ) + farmed_coin = coin_store.farm_coin(pool_reward_puzzle_hash, now) + now.seconds += 500 + now.height += 1 + + p2_singleton_coins = filter_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, [farmed_coin]) + assert p2_singleton_coins == [farmed_coin] + + assert len(list(coin_store.all_unspent_coins())) == 2 + + ####### + # now collect the `p2_singleton` for the pool + + for coin in p2_singleton_coins: + p2_singleton_coin_spend, singleton_conditions = claim_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, coin) + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions( + PUZZLE_DB, + pool_member_spend_type="claim-p2-nft", + pool_reward_amount=p2_singleton_coin_spend.coin.amount, + pool_reward_height=now.height - 1, + ) + spend_bundle = SpendBundle([coin_spend, p2_singleton_coin_spend], G2Element()) + spend_bundle.debug() + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + now.seconds += 500 + now.height += 1 + + SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + assert len(list(coin_store.all_unspent_coins())) == 2 + + ####### + # spend the singleton into the "leaving the pool" state + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions( + PUZZLE_DB, pool_member_spend_type="to-waiting-room", key_value_list=Program.to([("foo", "bar")]) + ) + spend_bundle = SpendBundle([coin_spend], G2Element()) + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + now.seconds += 500 + now.height += 1 + change_count = SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + assert change_count == 1 + + assert len(list(coin_store.all_unspent_coins())) == 2 + + ####### + # farm a `p2_singleton` + + pool_reward_puzzle_hash = p2_singleton_puzzle_hash_for_launcher( + PUZZLE_DB, SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, DELAY_SECONDS, DELAY_PUZZLE_HASH + ) + farmed_coin = coin_store.farm_coin(pool_reward_puzzle_hash, now) + now.seconds += 500 + now.height += 1 + + p2_singleton_coins = filter_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, [farmed_coin]) + assert p2_singleton_coins == [farmed_coin] + + assert len(list(coin_store.all_unspent_coins())) == 3 + + ####### + # now collect the `p2_singleton` for the pool + + for coin in p2_singleton_coins: + p2_singleton_coin_spend, singleton_conditions = claim_p2_singleton(PUZZLE_DB, SINGLETON_WALLET, coin) + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions( + PUZZLE_DB, + pool_leaving_spend_type="claim-p2-nft", + pool_reward_amount=p2_singleton_coin_spend.coin.amount, + pool_reward_height=now.height - 1, + ) + spend_bundle = SpendBundle([coin_spend, p2_singleton_coin_spend], G2Element()) + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + now.seconds += 500 + now.height += 1 + + SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + assert len(list(coin_store.all_unspent_coins())) == 3 + + ####### + # now finish leaving the pool + + initial_singleton_puzzle = adaptor_for_singleton_inner_puzzle(ANYONE_CAN_SPEND_PUZZLE) + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions( + PUZZLE_DB, + pool_leaving_spend_type="exit-waiting-room", + key_value_list=[("foo1", "bar2"), ("foo2", "baz5")], + destination_puzzle_hash=initial_singleton_puzzle.get_tree_hash(), + ) + spend_bundle = SpendBundle([coin_spend], G2Element()) + + full_puzzle = singleton_puzzle( + SINGLETON_WALLET.launcher_id, SINGLETON_WALLET.launcher_puzzle_hash, initial_singleton_puzzle + ) + + PUZZLE_DB.add_puzzle(full_puzzle) + + try: + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + assert 0 + except BadSpendBundleError as ex: + assert ex.args[0] == "condition validation failure Err.ASSERT_HEIGHT_RELATIVE_FAILED" + + now.seconds += 350000 + now.height += 1445 + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + + SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + assert len(list(coin_store.all_unspent_coins())) == 3 + + ####### + # now spend to oblivion with the `-113` hack + + coin_spend = SINGLETON_WALLET.coin_spend_for_conditions( + PUZZLE_DB, conditions=[[ConditionOpcode.CREATE_COIN, 0, -113]] + ) + spend_bundle = SpendBundle([coin_spend], G2Element()) + spend_bundle.debug() + + additions, removals = coin_store.update_coin_store_for_spend_bundle(spend_bundle, now, MAX_BLOCK_COST_CLVM) + update_count = SINGLETON_WALLET.update_state(PUZZLE_DB, removals) + + assert update_count == 0 + + assert len(list(coin_store.all_unspent_coins())) == 2 + + return 0 diff --git a/tests/wallet/test_wallet_interested_store.py b/tests/wallet/test_wallet_interested_store.py new file mode 100644 index 000000000000..0f2ae9926190 --- /dev/null +++ b/tests/wallet/test_wallet_interested_store.py @@ -0,0 +1,59 @@ +import asyncio +from pathlib import Path +from secrets import token_bytes +import aiosqlite +import pytest + +from chia.types.blockchain_format.coin import Coin +from chia.util.db_wrapper import DBWrapper +from chia.util.ints import uint64 + +from chia.wallet.wallet_interested_store import WalletInterestedStore + + +@pytest.fixture(scope="module") +def event_loop(): + loop = asyncio.get_event_loop() + yield loop + + +class TestWalletInterestedStore: + @pytest.mark.asyncio + async def test_store(self): + db_filename = Path("wallet_store_test.db") + + if db_filename.exists(): + db_filename.unlink() + + db_connection = await aiosqlite.connect(db_filename) + db_wrapper = DBWrapper(db_connection) + store = await WalletInterestedStore.create(db_wrapper) + try: + coin_1 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) + coin_2 = Coin(token_bytes(32), token_bytes(32), uint64(12312)) + assert (await store.get_interested_coin_ids()) == [] + await store.add_interested_coin_id(coin_1.name()) + assert (await store.get_interested_coin_ids()) == [coin_1.name()] + await store.add_interested_coin_id(coin_1.name()) + assert (await store.get_interested_coin_ids()) == [coin_1.name()] + await store.add_interested_coin_id(coin_2.name()) + assert set(await store.get_interested_coin_ids()) == {coin_1.name(), coin_2.name()} + puzzle_hash = token_bytes(32) + assert len(await store.get_interested_puzzle_hashes()) == 0 + + await store.add_interested_puzzle_hash(puzzle_hash, 2) + assert len(await store.get_interested_puzzle_hashes()) == 1 + await store.add_interested_puzzle_hash(puzzle_hash, 2) + assert len(await store.get_interested_puzzle_hashes()) == 1 + assert (await store.get_interested_puzzle_hash_wallet_id(puzzle_hash)) == 2 + await store.add_interested_puzzle_hash(puzzle_hash, 3) + assert len(await store.get_interested_puzzle_hashes()) == 1 + + assert (await store.get_interested_puzzle_hash_wallet_id(puzzle_hash)) == 3 + await store.remove_interested_puzzle_hash(puzzle_hash) + assert (await store.get_interested_puzzle_hash_wallet_id(puzzle_hash)) is None + assert len(await store.get_interested_puzzle_hashes()) == 0 + + finally: + await db_connection.close() + db_filename.unlink()